diff --git a/.gitignore b/.gitignore index 0215164..41e616a 100644 --- a/.gitignore +++ b/.gitignore @@ -9,8 +9,11 @@ tasks_.json tasks2.csv tasks2.xlsx tasks2.json +tasks_all.csv +tasks_all.xlsx # Directories used during unit testing +qvfs qvfs_1 qvfs_2 qvfs_3 diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 0613328..987586e 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1 +1 @@ -{".":"3.14.0","src":"3.12.0"} +{ ".": "3.14.0", "src": "3.12.0" } diff --git a/.vscode/launch.json b/.vscode/launch.json index 7a2aeec..e84315d 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -17,42 +17,59 @@ // ------------------------------------ // "args": [ // "task-custom-property-set", - // "--auth-type", - // "cert", // "--host", // "192.168.100.109", - // "--auth-cert-file", - // "./cert/client.pem", - // "--auth-cert-key-file", - // "./cert/client_key.pem", + + // "--port", + // // "4747", + // "443", + + // "--virtual-proxy", + // "jwt", + // "--auth-type", + // "jwt", + // "--auth-jwt", + // "", + + // // "--auth-cert-file", + // // "./cert/client.pem", + // // "--auth-cert-key-file", + // // "./cert/client_key.pem", + // "--auth-user-dir", // "LAB", // "--auth-user-id", // "goran", + // "--task-id", - // "e3b27f50-b1c0-4879-88fc-c7cdd9c1cf3e", - // "7552d9fc-d1bb-4975-9a38-18357de531ea", - // // "82bc3e66-c899-4e44-b52f-552145da5ee0", + // "82bc3e66-c899-4e44-b52f-552145da5ee0", + // "5748afa9-3abe-43ab-bb1f-127c48ced075", + // "5520e710-91ad-41d2-aeb6-434cafbf366b", // // "5748afa9-3abe-43ab-bb1f-127c48ced075", // // "5520e710-91ad-41d2-aeb6-434cafbf366b", - // // "--task-tag", + // "--task-tag", // // "Test data", // // "api1", + // "apiCreaeted", // "--custom-property-name", - // // "Department", + // "Department", // // "Department2", // // "DemoApp", - // "ReportMonth", + // // "ReportMonth", + // "--custom-property-value", - // "2023 Jan", - // // "Finance", - // // "Sales", - // "--qvf-overwrite", + // // "2023 Jan", + // "Finance", + // "Sales", + // "--update-mode", // // "append", // "replace", - // // "--dry-run" + + // "--overwrite", + + // "--dry-run" // ] // ------------------------------------ @@ -60,14 +77,25 @@ // ------------------------------------ // "args": [ // "task-import", - // "--auth-type", - // "cert", // "--host", // "192.168.100.109", - // "--auth-cert-file", - // "./cert/client.pem", - // "--auth-cert-key-file", - // "./cert/client_key.pem", + + // "--port", + // // "4747", + // "443", + + // "--virtual-proxy", + // "jwt", + // "--auth-type", + // "jwt", + // "--auth-jwt", + // "", + + // // "--auth-cert-file", + // // "./cert/client.pem", + // // "--auth-cert-key-file", + // // "./cert/client_key.pem", + // "--auth-user-dir", // "LAB", // "--auth-user-id", @@ -79,7 +107,7 @@ // "--file-name", // "testdata/tasks.xlsx", // "--sheet-name", - // "7" + // "1" // // "--import-app", // // "--import-app-sheet-name", @@ -100,69 +128,80 @@ // ------------------------------------ // Import tasks from CSV file // ------------------------------------ - "args": [ - "task-import", - "--auth-type", - "cert", + // "args": [ + // "task-import", + // "--auth-type", + // "cert", - // p2w1 - "--host", - "192.168.100.109", - "--auth-cert-file", - // "./cert/client.pem", - "../../code/secret/pro2win1-nopwd/client.pem", - "--auth-cert-key-file", - "../../code/secret/pro2win1-nopwd/client_key.pem", - // "./cert/client_key.pem", - "--auth-user-dir", - "LAB", - "--auth-user-id", - "goran", + // // p2w1 + // "--host", + // "192.168.100.109", + // "--auth-cert-file", + // // "./cert/client.pem", + // "../../code/secret/pro2win1-nopwd/client.pem", + // "--auth-cert-key-file", + // "../../code/secret/pro2win1-nopwd/client_key.pem", + // // "./cert/client_key.pem", + // "--auth-user-dir", + // "LAB", + // "--auth-user-id", + // "goran", - // Parallels - // "--host", - // "10.211.55.15", - // "--auth-cert-file", - // "../../code/secret/winsrv.local/client.pem", - // "--auth-cert-key-file", - // "../../code/secret/winsrv.local/client_key.pem", - // "--auth-user-dir", - // "winsrv1", - // "--auth-user-id", - // "goran", + // // Parallels + // // "--host", + // // "10.211.55.15", + // // "--auth-cert-file", + // // "../../code/secret/winsrv.local/client.pem", + // // "--auth-cert-key-file", + // // "../../code/secret/winsrv.local/client_key.pem", + // // "--auth-user-dir", + // // "winsrv1", + // // "--auth-user-id", + // // "goran", - "--file-type", - "csv", + // "--file-type", + // "csv", - "--file-name", - // "tasks2source.csv", - // "task-chain.csv", - // "testdata/reload-tasks.csv", - // "./tasks_all.csv", - "./testdata/tasks-1.csv" + // "--file-name", + // // "tasks2source.csv", + // // "task-chain.csv", + // // "testdata/reload-tasks.csv", + // // "./tasks_all.csv", + // "./testdata/tasks-1.csv" - // "--qvf-overwrite", - // "no", + // // "--qvf-overwrite", + // // "no", - // "--limit-import-count", - // "2", + // // "--limit-import-count", + // // "2", - // "--dry-run" - ] + // // "--dry-run" + // ] // ------------------------------------ // Export apps to QVF files // ------------------------------------ // "args": [ // "app-export", - // "--auth-type", - // "cert", // "--host", // "192.168.100.109", - // "--auth-cert-file", - // "./cert/client.pem", - // "--auth-cert-key-file", - // "./cert/client_key.pem", + + // "--port", + // // "4747", + // "443", + + // "--virtual-proxy", + // "jwt", + // "--auth-type", + // "jwt", + // "--auth-jwt", + // "", + + // // "--auth-cert-file", + // // "./cert/client.pem", + // // "--auth-cert-key-file", + // // "./cert/client_key.pem", + // "--auth-user-dir", // "LAB", // "--auth-user-id", @@ -206,37 +245,48 @@ // ------------------------------------ // Import apps from Excel file // ------------------------------------ - // "args": [ - // "app-import", - // "--auth-type", - // "cert", - // "--host", - // "192.168.100.109", - // "--auth-cert-file", - // "./cert/client.pem", - // "--auth-cert-key-file", - // "./cert/client_key.pem", - // "--auth-user-dir", - // "LAB", - // "--auth-user-id", - // "goran", + "args": [ + "app-import", + "--host", + "192.168.100.109", - // // "--file-type", - // // "excel", + "--port", + // "4747", + "443", - // "--file-name", - // "testdata/tasks.xlsx", - // "--sheet-name", - // "App-import", + "--virtual-proxy", + "jwt", + "--auth-type", + "jwt", + "--auth-jwt", + "", - // // "--limit-import-count", - // // "2", + // "--auth-cert-file", + // "./cert/client.pem", + // "--auth-cert-key-file", + // "./cert/client_key.pem", + + "--auth-user-dir", + "LAB", + "--auth-user-id", + "goran", - // "--sleep-app-upload", - // "500" + // "--file-type", + // "excel", - // // "--dry-run" - // ] + "--file-name", + "testdata/tasks.xlsx", + "--sheet-name", + "App import", + + // "--limit-import-count", + // "2", + + "--sleep-app-upload", + "500" + + // "--dry-run" + ] // ------------------------------------ // Get task tree @@ -283,20 +333,31 @@ // ] // ------------------------------------ - // Get reload tasks as table + // Get tasks as table // ------------------------------------ // "args": [ // "task-get", - // "--auth-type", - // "cert", // "--host", // "192.168.100.109", - // "--auth-cert-file", - // "../../code/secret/pro2win1-nopwd/client.pem", - // // "./cert/client.pem", - // "--auth-cert-key-file", - // "../../code/secret/pro2win1-nopwd/client_key.pem", - // // "./cert/client_key.pem", + + // "--port", + // // "4747", + // "443", + + // "--virtual-proxy", + // "jwt", + // "--auth-type", + // "jwt", + // "--auth-jwt", + // "", + + // // "--auth-cert-file", + // // "../../code/secret/pro2win1-nopwd/client.pem", + // // // "./cert/client.pem", + // // "--auth-cert-key-file", + // // "../../code/secret/pro2win1-nopwd/client_key.pem", + // // // "./cert/client_key.pem", + // "--auth-user-dir", // "LAB", // "--auth-user-id", @@ -304,15 +365,15 @@ // // "--task-type", // // "reload", - // "ext-program", + // // "ext-program", // // "--task-id", // // "afc250bc-28c3-49a2-8d63-80966749abe3", // // "5748afa9-3abe-43ab-bb1f-127c48ced075", // // "5520e710-91ad-41d2-aeb6-434cafbf366b", // "--output-format", - // // "table", - // "tree", + // "table", + // // "tree", // "--output-dest", // "screen", @@ -327,8 +388,8 @@ // // "--text-color", // // "no", - // // "--table-details", - // // "common", + // "--table-details", + // "common", // // "lastexecution", // // "tag", // // "customproperty", @@ -337,7 +398,7 @@ // ] // ------------------------------------ - // Get reload tasks as CSV/Excel/JSON file + // Get tasks as CSV/Excel/JSON file // ------------------------------------ // "args": [ // "task-get", @@ -438,19 +499,38 @@ // "cert", // "--host", // "192.168.100.109", - // "--app-id", - // "2933711d-6638-41d4-a2d2-6dd2d965208b", // Ctrl-Q CLI app - // // "a3e0f5d2-000a-464f-998d-33d333b175d7", // Thumbnail demo app + + // "--port", + // "443", + + // "--virtual-proxy", + // "jwt", + // "--auth-type", + // "jwt", + // "--auth-jwt", + // "", + + // // "--auth-cert-file", + // // "./cert/client.pem", + // // "--auth-cert-key-file", + // // "./cert/client_key.pem", + // "--auth-user-dir", // "LAB", // "--auth-user-id", // "goran", + + // "--app-id", + // "2933711d-6638-41d4-a2d2-6dd2d965208b", // Ctrl-Q CLI app + // // "a3e0f5d2-000a-464f-998d-33d333b175d7", // Thumbnail demo app + // "--file-type", // "excel", // "--file", // "./testdata/ctrl-q-master-items.xlsx", // "--sheet", // "Sales", + // "--col-ref-by", // "name", // "--col-item-type", @@ -469,8 +549,11 @@ // "Color", // "--col-master-item-per-value-color", // "Per value color", - // // "--sleep-between-imports", + + // "--sleep-between-imports", // // "1000", + // "0", + // // "--limit-import-count", // // "5", // "--log-level", @@ -484,15 +567,27 @@ // "master-item-measure-get", // "--host", // "192.168.100.109", + + // "--port", + // "443", + + // "--virtual-proxy", + // "jwt", + // "--auth-type", + // "jwt", + // "--auth-jwt", + // "", + // "--app-id", // "a3e0f5d2-000a-464f-998d-33d333b175d7", // // "2933711d-6638-41d4-a2d2-6dd2d965208b", - // "--id-type", - // "id", - // "--master-item", - // "e392f53f-17b5-4ede-8fd1-f64b4fae630f", + // // "--id-type", + // // "id", + // // "--master-item", + // // "e392f53f-17b5-4ede-8fd1-f64b4fae630f", // "--output-format", // "table", + // "--auth-user-dir", // "LAB", // "--auth-user-id", @@ -504,13 +599,23 @@ // ------------------------------------ // "args": [ // "master-item-measure-delete", - // "--auth-type", - // "cert", // "--host", // "192.168.100.109", + + // "--port", + // "443", + + // "--virtual-proxy", + // "jwt", + // "--auth-type", + // "jwt", + // "--auth-jwt", + // "", + // "--app-id", // "2933711d-6638-41d4-a2d2-6dd2d965208b", // "--auth-user-dir", + // "LAB", // "--auth-user-id", // "goran", @@ -529,11 +634,24 @@ // "master-item-dim-get", // "--host", // "192.168.100.109", + + // "--port", + // "443", + + // "--virtual-proxy", + // "jwt", + // "--auth-type", + // "jwt", + // "--auth-jwt", + // "", + // "--app-id", // // "a3e0f5d2-000a-464f-998d-33d333b175d7", // "2933711d-6638-41d4-a2d2-6dd2d965208b", + // "--output-format", // "table", + // "--auth-user-dir", // "LAB", // "--auth-user-id", @@ -545,16 +663,27 @@ // ------------------------------------ // "args": [ // "master-item-dim-delete", - // "--auth-type", - // "cert", // "--host", // "192.168.100.109", + + // "--port", + // "443", + + // "--virtual-proxy", + // "jwt", + // "--auth-type", + // "jwt", + // "--auth-jwt", + // "", + // "--app-id", // "2933711d-6638-41d4-a2d2-6dd2d965208b", + // "--auth-user-dir", // "LAB", // "--auth-user-id", // "goran", + // "--id-type", // "name", // "--master-item", @@ -569,18 +698,34 @@ // "variable-get", // "--host", // "192.168.100.109", + // "--qrs-port", - // "4242", + // // "4242", + // "443", + + // "--engine-port", + // // "4747", + // "443", + + // "--virtual-proxy", + // "jwt", + // "--auth-type", + // "jwt", + // "--auth-jwt", + // "", + // "--app-id", // // "a3e0f5d2-000a-464f-998d-33d333b175d7", // "2933711d-6638-41d4-a2d2-6dd2d965208b", // // "--app-tag", // // "Ctrl-Q variable 1", // // "Ctrl-Q variable 2", + // "--auth-user-dir", // "LAB", // "--auth-user-id", // "goran", + // // "--id-type", // // "id", // // "--variable", @@ -594,8 +739,10 @@ // // // "TimestampFormat", // // // "var1", // // "vVar1", + // "--output-format", // "table", + // // "--log-level", // // "info" // ] @@ -603,30 +750,45 @@ // ------------------------------------ // Delete variables // ------------------------------------ - // "args": [ + // "args": [ // "variable-delete", - // "--auth-type", - // "cert", // "--host", // "192.168.100.109", - // // "--qrs-port", + + // "--qrs-port", // // "4242", + // "443", + + // "--engine-port", + // // "4747", + // "443", + + // "--virtual-proxy", + // "jwt", + // "--auth-type", + // "jwt", + // "--auth-jwt", + // "", + // "--app-id", // // "a3e0f5d2-000a-464f-998d-33d333b175d7", // "2933711d-6638-41d4-a2d2-6dd2d965208b", // // "--app-tag", // // "Ctrl-Q variable 1", // // "Ctrl-Q variable 2", + // "--auth-user-dir", // "LAB", // "--auth-user-id", // "goran", + // // "--id-type", // // "id", // // "--variable", // // "2a70741b-6fdc-4252-a30d-7a2b2b4403a7", // // "2a70741b-6fdc-4252-a30d-7a2b2b4403a6", // // "2a70741b-6fdc-4252-a30d-7a2b2b4403a8", + // // "--id-type", // // "name", // // "--variable", @@ -643,16 +805,28 @@ // ------------------------------------ // "args": [ // "bookmark-get", - // "--auth-type", - // "cert", // "--host", // "192.168.100.109", + + // "--port", + // // "4747", + // "443", + + // "--virtual-proxy", + // "jwt", + // "--auth-type", + // "jwt", + // "--auth-jwt", + // "", + // "--app-id", // "2933711d-6638-41d4-a2d2-6dd2d965208b", + // "--auth-user-dir", // "LAB", // "--auth-user-id", // "goran", + // "--id-type", // "id", // // "--bookmark", @@ -757,15 +931,30 @@ // "field-scramble", // "--host", // "192.168.100.109", + + // "--port", + // // "4747", + // "443", + + // "--virtual-proxy", + // "jwt", + // "--auth-type", + // "jwt", + // "--auth-jwt", + // "", + // "--app-id", // "2933711d-6638-41d4-a2d2-6dd2d965208b", + // "--auth-user-dir", // "LAB", // "--auth-user-id", // "goran", + // "--field-name", // "Dim4", // "Dim2", + // "--new-app-name", // "'New scrambled app'" // ] @@ -774,17 +963,31 @@ // Get script // ------------------------------------ // "args": [ - // "getscript", + // "script-get", // "--host", // "192.168.100.109", + + // "--port", + // // "4747", + // "443", + + // "--virtual-proxy", + // "jwt", + // "--auth-type", + // "jwt", + // "--auth-jwt", + // "", + // "--app-id", // "deba4bcf-47e4-472e-97b2-4fe8d6498e11", + // "--auth-user-dir", // "LAB", // "--auth-user-id", // "goran", + // "--log-level", - // "verbose" + // "info" // ] // ------------------------------------ @@ -794,12 +997,32 @@ // "connection-test", // "--host", // "192.168.100.109", + // // "--port", + // // "443", + + // // "--virtual-proxy", + // // "/jwt", + // // "/jwt", + + // // "--auth-type", + // // "jwt", + // // "cert", + // // "--host", + // // "192.168.100.109", + // // "--auth-cert-file", + // // "./cert/client.pem", + // // "--auth-cert-key-file", + // // "./cert/client_key.pem", + // // "--auth-jwt", + // // "....", + // "--auth-user-dir", // "LAB", // "--auth-user-id", // "goran", - // "--log-level", - // "info" + + // // "--log-level", + // // "info" // ] } ] diff --git a/CHANGELOG.md b/CHANGELOG.md index 64264d0..d636cb0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,38 +2,34 @@ ## [3.14.0](https://github.com/ptarmiganlabs/ctrl-q/compare/v3.13.2...v3.14.0) (2023-11-19) - ### Features -* **connection-test:** Add command to test connection to Sense server ([328886e](https://github.com/ptarmiganlabs/ctrl-q/commit/328886e9ec92a553a732ed6f7f0d17bacfd9b2dc)), closes [#328](https://github.com/ptarmiganlabs/ctrl-q/issues/328) -* **docs:** Move all documentation to ctrl-q.ptarmiganlabs.com ([e60dc31](https://github.com/ptarmiganlabs/ctrl-q/commit/e60dc31c27c889524e0e634ac03c6517285783d6)) -* **task-get:** Simplify --table-details option ([2cbd470](https://github.com/ptarmiganlabs/ctrl-q/commit/2cbd4704213ba4a043662b462e980f7a47152553)), closes [#345](https://github.com/ptarmiganlabs/ctrl-q/issues/345) -* **task-get:** Sort tasks in task tree alphabetically using task name ([ca96d4c](https://github.com/ptarmiganlabs/ctrl-q/commit/ca96d4cb0648b71f437bfe457855e9adbc2f0a9d)) -* **task-import:** Support external program tasks when importing tasks ([8060a1b](https://github.com/ptarmiganlabs/ctrl-q/commit/8060a1bda84a685ef150c8e46d8adb0d13cd3f46)) - +- **connection-test:** Add command to test connection to Sense server ([328886e](https://github.com/ptarmiganlabs/ctrl-q/commit/328886e9ec92a553a732ed6f7f0d17bacfd9b2dc)), closes [#328](https://github.com/ptarmiganlabs/ctrl-q/issues/328) +- **docs:** Move all documentation to ctrl-q.ptarmiganlabs.com ([e60dc31](https://github.com/ptarmiganlabs/ctrl-q/commit/e60dc31c27c889524e0e634ac03c6517285783d6)) +- **task-get:** Simplify --table-details option ([2cbd470](https://github.com/ptarmiganlabs/ctrl-q/commit/2cbd4704213ba4a043662b462e980f7a47152553)), closes [#345](https://github.com/ptarmiganlabs/ctrl-q/issues/345) +- **task-get:** Sort tasks in task tree alphabetically using task name ([ca96d4c](https://github.com/ptarmiganlabs/ctrl-q/commit/ca96d4cb0648b71f437bfe457855e9adbc2f0a9d)) +- **task-import:** Support external program tasks when importing tasks ([8060a1b](https://github.com/ptarmiganlabs/ctrl-q/commit/8060a1bda84a685ef150c8e46d8adb0d13cd3f46)) ### Bug Fixes -* Fix broken CI badge in readme file ([57cfae9](https://github.com/ptarmiganlabs/ctrl-q/commit/57cfae9933fdc71f24172d488aae10a0ba6e924a)) -* **task-get:** --table-details wo parameters now return all task details in table ([1a9a587](https://github.com/ptarmiganlabs/ctrl-q/commit/1a9a587a641eac2aa7328f39421218b3973c8f83)), closes [#332](https://github.com/ptarmiganlabs/ctrl-q/issues/332) -* **task-get:** --task-type option is now invalid for task trees ([1bddb6a](https://github.com/ptarmiganlabs/ctrl-q/commit/1bddb6a1dc685e50f6636a6192d49b59a8ed7837)) -* **task-get:** Add better debug logging when showing task trees ([c66ab77](https://github.com/ptarmiganlabs/ctrl-q/commit/c66ab77fd88cfa2c3a336f10a040dc18a94c49c4)) -* **task-get:** No more duplicate, top-level schema tasks in task tree, for a specfic task. ([d3fe908](https://github.com/ptarmiganlabs/ctrl-q/commit/d3fe9087758ed0476552f2bf2ef86bf77693d0b8)), closes [#333](https://github.com/ptarmiganlabs/ctrl-q/issues/333) -* **task-import:** Correctly handle upstream ext pgm tasks in reload task composite events ([53e076b](https://github.com/ptarmiganlabs/ctrl-q/commit/53e076b5742a2412cffb9176431b2476af996b75)), closes [#331](https://github.com/ptarmiganlabs/ctrl-q/issues/331) -* **task-import:** Importing tasks from CSV file no longer gives "premature close" ([e06f1a9](https://github.com/ptarmiganlabs/ctrl-q/commit/e06f1a9dc6499d93ddc192e720c46c1e3af2da7c)), closes [#323](https://github.com/ptarmiganlabs/ctrl-q/issues/323) - +- Fix broken CI badge in readme file ([57cfae9](https://github.com/ptarmiganlabs/ctrl-q/commit/57cfae9933fdc71f24172d488aae10a0ba6e924a)) +- **task-get:** --table-details wo parameters now return all task details in table ([1a9a587](https://github.com/ptarmiganlabs/ctrl-q/commit/1a9a587a641eac2aa7328f39421218b3973c8f83)), closes [#332](https://github.com/ptarmiganlabs/ctrl-q/issues/332) +- **task-get:** --task-type option is now invalid for task trees ([1bddb6a](https://github.com/ptarmiganlabs/ctrl-q/commit/1bddb6a1dc685e50f6636a6192d49b59a8ed7837)) +- **task-get:** Add better debug logging when showing task trees ([c66ab77](https://github.com/ptarmiganlabs/ctrl-q/commit/c66ab77fd88cfa2c3a336f10a040dc18a94c49c4)) +- **task-get:** No more duplicate, top-level schema tasks in task tree, for a specfic task. ([d3fe908](https://github.com/ptarmiganlabs/ctrl-q/commit/d3fe9087758ed0476552f2bf2ef86bf77693d0b8)), closes [#333](https://github.com/ptarmiganlabs/ctrl-q/issues/333) +- **task-import:** Correctly handle upstream ext pgm tasks in reload task composite events ([53e076b](https://github.com/ptarmiganlabs/ctrl-q/commit/53e076b5742a2412cffb9176431b2476af996b75)), closes [#331](https://github.com/ptarmiganlabs/ctrl-q/issues/331) +- **task-import:** Importing tasks from CSV file no longer gives "premature close" ([e06f1a9](https://github.com/ptarmiganlabs/ctrl-q/commit/e06f1a9dc6499d93ddc192e720c46c1e3af2da7c)), closes [#323](https://github.com/ptarmiganlabs/ctrl-q/issues/323) ### Miscellaneous -* Add unit tests ([50bbae1](https://github.com/ptarmiganlabs/ctrl-q/commit/50bbae1548d59f27cc51f6bafff16483a8ee2c0c)) -* **deps:** update actions/setup-node action to v4 ([aea1d71](https://github.com/ptarmiganlabs/ctrl-q/commit/aea1d713f2ed36ece503d1e3a11282d9bc269d17)) -* **deps:** Update dependencies ([5351e29](https://github.com/ptarmiganlabs/ctrl-q/commit/5351e296bd373f43d7b58ccd6e430a199721398b)) -* **deps:** Update dependencies to stay safe and secure ([568a25f](https://github.com/ptarmiganlabs/ctrl-q/commit/568a25fa6c7b34bdf430dc080e776fed2e261a74)) - +- Add unit tests ([50bbae1](https://github.com/ptarmiganlabs/ctrl-q/commit/50bbae1548d59f27cc51f6bafff16483a8ee2c0c)) +- **deps:** update actions/setup-node action to v4 ([aea1d71](https://github.com/ptarmiganlabs/ctrl-q/commit/aea1d713f2ed36ece503d1e3a11282d9bc269d17)) +- **deps:** Update dependencies ([5351e29](https://github.com/ptarmiganlabs/ctrl-q/commit/5351e296bd373f43d7b58ccd6e430a199721398b)) +- **deps:** Update dependencies to stay safe and secure ([568a25f](https://github.com/ptarmiganlabs/ctrl-q/commit/568a25fa6c7b34bdf430dc080e776fed2e261a74)) ### Documentation -* Add more task import examples ([0ca7363](https://github.com/ptarmiganlabs/ctrl-q/commit/0ca7363038f635a0361cee95c1a8a1145e62be1a)) +- Add more task import examples ([0ca7363](https://github.com/ptarmiganlabs/ctrl-q/commit/0ca7363038f635a0361cee95c1a8a1145e62be1a)) ## [3.13.2](https://github.com/ptarmiganlabs/ctrl-q/compare/v3.13.1...v3.13.2) (2023-10-06) diff --git a/src/__tests__/app.test.js b/src/__tests__/app_cert.test.js similarity index 96% rename from src/__tests__/app.test.js rename to src/__tests__/app_cert.test.js index d4389d2..11526f7 100644 --- a/src/__tests__/app.test.js +++ b/src/__tests__/app_cert.test.js @@ -37,7 +37,7 @@ const nonExistingAppId1 = '9f0d0e02-cccc-bbbb-aaaa-3e9a4d0c8a3d'; const tag1 = 'Test data'; // Get one app by ID -describe('getAppById', () => { +describe('getAppById (cert auth)', () => { test('existing app ID', async () => { const result = await getAppById(existingAppId1, options); expect(result.id).toBe(existingAppId1); @@ -50,7 +50,7 @@ describe('getAppById', () => { }); // Get one or more apps by ID and/or tag -describe('getApps', () => { +describe('getApps (cert auth)', () => { test('one app ID, no tags', async () => { const result = await getApps(options, [existingAppId1]); // error(`Result: ${JSON.stringify(result)}`); diff --git a/src/__tests__/app_jwt.test.js b/src/__tests__/app_jwt.test.js new file mode 100644 index 0000000..e16f549 --- /dev/null +++ b/src/__tests__/app_jwt.test.js @@ -0,0 +1,122 @@ +/* eslint-disable no-console */ +const { test, expect, describe } = require('@jest/globals'); + +const { getApps, getAppById, appExistById, deleteAppById } = require('../lib/util/app'); +const { importAppFromFile } = require('../lib/cmd/importapp'); +const { sleep } = require('../globals'); + +const options = { + logLevel: process.env.CTRL_Q_LOG_LEVEL || 'info', + authType: process.env.CTRL_Q_AUTH_TYPE || 'cert', + host: process.env.CTRL_Q_HOST || '', + port: process.env.CTRL_Q_PORT || '4242', + schemaVersion: process.env.CTRL_Q_SCHEMA_VERSION || '12.612.0', + virtualProxy: process.env.CTRL_Q_VIRTUAL_PROXY || '', + secure: process.env.CTRL_Q_SECURE || true, + authUserDir: process.env.CTRL_Q_AUTH_USER_DIR || '', + authUserId: process.env.CTRL_Q_AUTH_USER_ID || '', + authJwt: process.env.CTRL_Q_AUTH_JWT || '', +}; + +const defaultTestTimeout = process.env.CTRL_Q_TEST_TIMEOUT || 600000; // 10 minute default timeout +jest.setTimeout(defaultTestTimeout); + +// Mock logger +// global.console = { +// log: jest.fn(), +// info: jest.fn(), +// error: jest.fn(), +// }; + +// Define existing and non-existing tasks +const existingAppId1 = 'c840670c-7178-4a5e-8409-ba2da69127e2'; +const existingAppId2 = '3a6c9a53-cb8d-42f3-a8ee-c083c1f8ed8e'; +const nonExistingAppId1 = '9f0d0e02-cccc-bbbb-aaaa-3e9a4d0c8a3d'; +const tag1 = 'Test data'; + +// Get one app by ID +describe('getAppById (JWT auth)', () => { + options.authType = 'jwt'; + options.port = '443'; + options.virtualProxy = 'jwt'; + + test('existing app ID', async () => { + const result = await getAppById(existingAppId1, options); + expect(result.id).toBe(existingAppId1); + }); + + test('non-existing app ID', async () => { + const result = await getAppById(nonExistingAppId1, options); + expect(result).toBe(false); + }); +}); + +// Get one or more apps by ID and/or tag +describe('getApps (JWT auth)', () => { + options.authType = 'jwt'; + options.port = '443'; + options.virtualProxy = 'jwt'; + + test('one app ID, no tags', async () => { + const result = await getApps(options, [existingAppId1]); + + expect(result.length).toBe(1); + expect(result[0].id).toBe(existingAppId1); + }); + + test('two app IDs, no tags', async () => { + const result = await getApps(options, [existingAppId1, existingAppId2]); + + expect(result.length).toBe(2); + }); + + test('no app IDs, one tag', async () => { + const result = await getApps(options, [], [tag1]); + + expect(result.length).toBeGreaterThan(0); + }); +}); + +// Delete an app given a valid app ID +describe('deleteAppById (JWT auth)', () => { + options.authType = 'jwt'; + options.port = '443'; + options.virtualProxy = 'jwt'; + options.fileType = 'excel'; + options.fileName = 'testdata/tasks.xlsx'; + options.sheetName = 'App import'; + options.sleepAppUpload = '500'; + + test('upload a few apps, then delete them', async () => { + // Upload apps + let result = await importAppFromFile(options); + expect(result.appList.length).toBeGreaterThan(0); + + // console.log('Sleeping 10 seconds'); + // await sleep(10000); + // console.log('Done sleeping'); + + // Loop over all apps in the appList + for (let i = 0; i < result.appList.length; ) { + // id of uploaded app + const appId = result.appList[i].appComplete.createdAppId; + // console.log(`App ID: ${appId}`); + + // Check if app exists + // eslint-disable-next-line no-await-in-loop + const appExists = await appExistById(appId, options); + + if (appExists) { + // Delete app + // eslint-disable-next-line no-await-in-loop + const resultDelete = await deleteAppById(appId, options); + expect(resultDelete).toBe(true); + } else { + // App does not exist + expect(appExists).toBe(false); + // console.log(`App ${appId} does not exist in Sense. Skipping delete.`); + } + i += 1; + } + }); +}); diff --git a/src/__tests__/connection_test_cert.test.js b/src/__tests__/connection_test_cert.test.js new file mode 100644 index 0000000..2224c51 --- /dev/null +++ b/src/__tests__/connection_test_cert.test.js @@ -0,0 +1,65 @@ +/* eslint-disable no-console */ +const { test, expect, describe } = require('@jest/globals'); + +const { testConnection } = require('../lib/cmd/testconnection'); + +const options = { + logLevel: process.env.CTRL_Q_LOG_LEVEL || 'info', + authType: process.env.CTRL_Q_AUTH_TYPE, + authCertFile: process.env.CTRL_Q_AUTH_CERT_FILE || './cert/client.pem', + authCertKeyFile: process.env.CTRL_Q_AUTH_CERT_KEY_FILE || './cert/client_key.pem', + host: process.env.CTRL_Q_HOST || '', + port: process.env.CTRL_Q_PORT || '', + virtualProxy: process.env.CTRL_Q_VIRTUAL_PROXY || '', + secure: process.env.CTRL_Q_SECURE || true, + authUserDir: process.env.CTRL_Q_AUTH_USER_DIR || '', + authUserId: process.env.CTRL_Q_AUTH_USER_ID || '', + authJwt: process.env.CTRL_Q_AUTH_JWT || '', +}; + +const defaultTestTimeout = process.env.CTRL_Q_TEST_TIMEOUT || 120000; // 2 minute default timeout +console.log(`Jest timeout: ${defaultTestTimeout}`); +jest.setTimeout(defaultTestTimeout); + +// Connection test using cert auth +describe('connection test (cert auth)', () => { + options.authType = 'cert'; + options.port = '4242'; + + test('Verify parameters', async () => { + expect(options.authType).toBe('cert'); + expect(options.authCertFile).not.toHaveLength(0); + expect(options.authCertKeyFile).not.toHaveLength(0); + expect(options.host).not.toHaveLength(0); + expect(options.authUserDir).not.toHaveLength(0); + expect(options.authUserId).not.toHaveLength(0); + }); + + /** + * Do connection test + * VP = + * Should succeed + */ + test('do connection test (virtual proxy=)', async () => { + options.virtualProxy = ''; + const result = await testConnection(options); + + // Result should be a JSON object + expect(result).toBeInstanceOf(Object); + expect(result.schemaPath).toBe('About'); + }); + + /** + * Do connection test + * VP = '/' + * Should succeed + */ + test('do connection test (virtual proxy=/)', async () => { + options.virtualProxy = '/'; + const result = await testConnection(options); + + // Result should be a JSON object + expect(result).toBeInstanceOf(Object); + expect(result.schemaPath).toBe('About'); + }); +}); diff --git a/src/__tests__/connection_test_jwt.test.js b/src/__tests__/connection_test_jwt.test.js new file mode 100644 index 0000000..3815757 --- /dev/null +++ b/src/__tests__/connection_test_jwt.test.js @@ -0,0 +1,65 @@ +/* eslint-disable no-console */ +const { test, expect, describe } = require('@jest/globals'); + +const { testConnection } = require('../lib/cmd/testconnection'); + +const options = { + logLevel: process.env.CTRL_Q_LOG_LEVEL || 'info', + authType: process.env.CTRL_Q_AUTH_TYPE, + authCertFile: process.env.CTRL_Q_AUTH_CERT_FILE || './cert/client.pem', + authCertKeyFile: process.env.CTRL_Q_AUTH_CERT_KEY_FILE || './cert/client_key.pem', + host: process.env.CTRL_Q_HOST || '', + port: process.env.CTRL_Q_PORT || '', + virtualProxy: process.env.CTRL_Q_VIRTUAL_PROXY || '', + secure: process.env.CTRL_Q_SECURE || true, + authUserDir: process.env.CTRL_Q_AUTH_USER_DIR || '', + authUserId: process.env.CTRL_Q_AUTH_USER_ID || '', + authJwt: process.env.CTRL_Q_AUTH_JWT || '', +}; + +const defaultTestTimeout = process.env.CTRL_Q_TEST_TIMEOUT || 120000; // 2 minute default timeout +console.log(`Jest timeout: ${defaultTestTimeout}`); +jest.setTimeout(defaultTestTimeout); + +// Connection test using JWT auth +describe('connection test (JWT auth)', () => { + options.authType = 'jwt'; + options.port = '443'; + options.virtualProxy = 'jwt'; + + test('Verify parameters', async () => { + expect(options.host).not.toHaveLength(0); + expect(options.port).not.toHaveLength(0); + expect(options.authJwt).not.toHaveLength(0); + }); + + /** + * Do connection test + * VP = 'jwt' + * Should succeed + */ + test('do connection test (virtual proxy=jwt)', async () => { + options.virtualProxy = 'jwt'; + + const result = await testConnection(options); + + // Result should be a JSON object + expect(result).toBeInstanceOf(Object); + expect(result.schemaPath).toBe('About'); + }); + + /** + * Do connection test + * VP = 'jwt' + * Should succeed + */ + test('do connection test (virtual proxy=/jwt)', async () => { + options.virtualProxy = '/jwt'; + + const result = await testConnection(options); + + // Result should be a JSON object + expect(result).toBeInstanceOf(Object); + expect(result.schemaPath).toBe('About'); + }); +}); diff --git a/src/__tests__/task_import.test.js b/src/__tests__/task_import.test.js index 1083f0f..ca4ec22 100644 --- a/src/__tests__/task_import.test.js +++ b/src/__tests__/task_import.test.js @@ -3,8 +3,8 @@ const { test, expect, describe } = require('@jest/globals'); const { importTaskFromFile } = require('../lib/cmd/importtask'); -const { getTaskById, deleteExternalProgramTaskById, deleteReloadTaskById, } = require('../lib/util/task'); -const { mapTaskType, } = require('../lib/util/lookups'); +const { getTaskById, deleteExternalProgramTaskById, deleteReloadTaskById } = require('../lib/util/task'); +const { mapTaskType } = require('../lib/util/lookups'); const options = { logLevel: process.env.CTRL_Q_LOG_LEVEL || 'info', @@ -96,7 +96,7 @@ describe('import task', () => { for (let i = 0; i < result.length; i += 1) { const task = result[i]; const { taskId } = task; - + await deleteReloadTaskById(taskId, options); } }); diff --git a/src/ctrl-q.js b/src/ctrl-q.js index 5194a50..00e4139 100644 --- a/src/ctrl-q.js +++ b/src/ctrl-q.js @@ -141,7 +141,7 @@ const program = new Command(); new Option('--log-level ', 'log level').choices(['error', 'warn', 'info', 'verbose', 'debug', 'silly']).default('info') ) .requiredOption('--host ', 'Qlik Sense server IP/FQDN') - .option('--port ', 'Qlik Sense server engine port', '4747') + .option('--port ', 'Qlik Sense server engine port (usually 4747 for cert auth, 443 for jwt auth)', '4747') .option('--schema-version ', 'Qlik Sense engine schema version', '12.612.0') .requiredOption('--app-id ', 'Qlik Sense app ID') .requiredOption('--virtual-proxy ', 'Qlik Sense virtual proxy prefix', '') @@ -149,10 +149,11 @@ const program = new Command(); .requiredOption('--auth-user-dir ', 'user directory for user to connect with') .requiredOption('--auth-user-id ', 'user ID for user to connect with') - .addOption(new Option('-a, --auth-type ', 'authentication type').choices(['cert']).default('cert')) + .addOption(new Option('-a, --auth-type ', 'authentication type').choices(['cert', 'jwt']).default('cert')) .option('--auth-cert-file ', 'Qlik Sense certificate file (exported from QMC)', './cert/client.pem') .option('--auth-cert-key-file ', 'Qlik Sense certificate key file (exported from QMC)', './cert/client_key.pem') .option('--auth-root-cert-file ', 'Qlik Sense root certificate file (exported from QMC)', './cert/root.pem') + .option('--auth-jwt ', 'JSON Web Token (JWT) to use for authentication with Qlik Sense server') .addOption(new Option('-t, --file-type ', 'source file type').choices(['excel']).default('excel')) .requiredOption('--file ', 'file containing master item definitions') @@ -228,7 +229,7 @@ const program = new Command(); new Option('--log-level ', 'log level').choices(['error', 'warn', 'info', 'verbose', 'debug', 'silly']).default('info') ) .requiredOption('--host ', 'Qlik Sense server IP/FQDN') - .option('--port ', 'Qlik Sense server engine port', '4747') + .option('--port ', 'Qlik Sense server engine port (usually 4747 for cert auth, 443 for jwt auth)', '4747') .option('--schema-version ', 'Qlik Sense engine schema version', '12.612.0') .requiredOption('--app-id ', 'Qlik Sense app ID') .requiredOption('--virtual-proxy ', 'Qlik Sense virtual proxy prefix', '') @@ -236,10 +237,11 @@ const program = new Command(); .requiredOption('--auth-user-dir ', 'user directory for user to connect with') .requiredOption('--auth-user-id ', 'user ID for user to connect with') - .addOption(new Option('-a, --auth-type ', 'authentication type').choices(['cert']).default('cert')) + .addOption(new Option('-a, --auth-type ', 'authentication type').choices(['cert', 'jwt']).default('cert')) .option('--auth-cert-file ', 'Qlik Sense certificate file (exported from QMC)', './cert/client.pem') .option('--auth-cert-key-file ', 'Qlik Sense certificate key file (exported from QMC)', './cert/client_key.pem') .option('--auth-root-cert-file ', 'Qlik Sense root certificate file (exported from QMC)', './cert/root.pem') + .option('--auth-jwt ', 'JSON Web Token (JWT) to use for authentication with Qlik Sense server') .addOption( new Option('--id-type ', 'type of identifier passed in the --master-item option').choices(['id', 'name']).default('name') @@ -261,7 +263,7 @@ const program = new Command(); new Option('--log-level ', 'log level').choices(['error', 'warn', 'info', 'verbose', 'debug', 'silly']).default('info') ) .requiredOption('--host ', 'Qlik Sense server IP/FQDN') - .option('--port ', 'Qlik Sense server engine port', '4747') + .option('--port ', 'Qlik Sense server engine port (usually 4747 for cert auth, 443 for jwt auth)', '4747') .option('--schema-version ', 'Qlik Sense engine schema version', '12.612.0') .requiredOption('--app-id ', 'Qlik Sense app ID') .requiredOption('--virtual-proxy ', 'Qlik Sense virtual proxy prefix', '') @@ -269,10 +271,11 @@ const program = new Command(); .requiredOption('--auth-user-dir ', 'user directory for user to connect with') .requiredOption('--auth-user-id ', 'user ID for user to connect with') - .addOption(new Option('-a, --auth-type ', 'authentication type').choices(['cert']).default('cert')) + .addOption(new Option('-a, --auth-type ', 'authentication type').choices(['cert', 'jwt']).default('cert')) .option('--auth-cert-file ', 'Qlik Sense certificate file (exported from QMC)', './cert/client.pem') .option('--auth-cert-key-file ', 'Qlik Sense certificate key file (exported from QMC)', './cert/client_key.pem') .option('--auth-root-cert-file ', 'Qlik Sense root certificate file (exported from QMC)', './cert/root.pem') + .option('--auth-jwt ', 'JSON Web Token (JWT) to use for authentication with Qlik Sense server') .addOption(new Option('--id-type ', 'type of identifier passed in the --master-item option').choices(['id', 'name'])) .option('--master-item ', 'names or IDs of master measures to be deleted. Multiple IDs should be space separated') @@ -292,7 +295,7 @@ const program = new Command(); new Option('--log-level ', 'log level').choices(['error', 'warn', 'info', 'verbose', 'debug', 'silly']).default('info') ) .requiredOption('--host ', 'Qlik Sense server IP/FQDN') - .option('--port ', 'Qlik Sense server engine port', '4747') + .option('--port ', 'Qlik Sense server engine port (usually 4747 for cert auth, 443 for jwt auth)', '4747') .option('--schema-version ', 'Qlik Sense engine schema version', '12.612.0') .requiredOption('--app-id ', 'Qlik Sense app ID') .requiredOption('--virtual-proxy ', 'Qlik Sense virtual proxy prefix', '') @@ -300,10 +303,11 @@ const program = new Command(); .requiredOption('--auth-user-dir ', 'user directory for user to connect with') .requiredOption('--auth-user-id ', 'user ID for user to connect with') - .addOption(new Option('-a, --auth-type ', 'authentication type').choices(['cert']).default('cert')) + .addOption(new Option('-a, --auth-type ', 'authentication type').choices(['cert', 'jwt']).default('cert')) .requiredOption('--auth-cert-file ', 'Qlik Sense certificate file (exported from QMC)', './cert/client.pem') .requiredOption('--auth-cert-key-file ', 'Qlik Sense certificate key file (exported from QMC)', './cert/client_key.pem') .requiredOption('--auth-root-cert-file ', 'Qlik Sense root certificate file (exported from QMC)', './cert/root.pem') + .option('--auth-jwt ', 'JSON Web Token (JWT) to use for authentication with Qlik Sense server') .addOption( new Option('--id-type ', 'type of identifier passed in the --master-item option').choices(['id', 'name']).default('name') @@ -325,7 +329,7 @@ const program = new Command(); new Option('--log-level ', 'log level').choices(['error', 'warn', 'info', 'verbose', 'debug', 'silly']).default('info') ) .requiredOption('--host ', 'Qlik Sense server IP/FQDN') - .option('--port ', 'Qlik Sense server engine port', '4747') + .option('--port ', 'Qlik Sense server engine port (usually 4747 for cert auth, 443 for jwt auth)', '4747') .option('--schema-version ', 'Qlik Sense engine schema version', '12.612.0') .requiredOption('--app-id ', 'Qlik Sense app ID') .requiredOption('--virtual-proxy ', 'Qlik Sense virtual proxy prefix', '') @@ -333,10 +337,11 @@ const program = new Command(); .requiredOption('--auth-user-dir ', 'user directory for user to connect with') .requiredOption('--auth-user-id ', 'user ID for user to connect with') - .addOption(new Option('-a, --auth-type ', 'authentication type').choices(['cert']).default('cert')) + .addOption(new Option('-a, --auth-type ', 'authentication type').choices(['cert', 'jwt']).default('cert')) .option('--auth-cert-file ', 'Qlik Sense certificate file (exported from QMC)', './cert/client.pem') .option('--auth-cert-key-file ', 'Qlik Sense certificate key file (exported from QMC)', './cert/client_key.pem') .option('--auth-root-cert-file ', 'Qlik Sense root certificate file (exported from QMC)', './cert/root.pem') + .option('--auth-jwt ', 'JSON Web Token (JWT) to use for authentication with Qlik Sense server') .addOption(new Option('--id-type ', 'type of identifier passed in the --master-item option').choices(['id', 'name'])) .option('--master-item ', 'names or IDs of master dimensions to be deleted. Multiple IDs should be space separated') @@ -357,8 +362,8 @@ const program = new Command(); new Option('--log-level ', 'log level').choices(['error', 'warn', 'info', 'verbose', 'debug', 'silly']).default('info') ) .requiredOption('--host ', 'Qlik Sense server IP/FQDN') - .option('--engine-port ', 'Qlik Sense server engine port', '4747') - .option('--qrs-port ', 'Qlik Sense repository service (QRS) port', '4242') + .option('--engine-port ', 'Qlik Sense server engine port (usually 4747 for cert auth, 443 for jwt auth)', '4747') + .option('--qrs-port ', 'Qlik Sense repository service (QRS) port (usually 4747 for cert auth, 443 for jwt auth)', '4242') .option('--schema-version ', 'Qlik Sense engine schema version', '12.612.0') .option('--app-id ', 'Qlik Sense app ID(s) to get variables from') .option('--app-tag ', 'Qlik Sense app tag(s) to get variables') @@ -367,10 +372,11 @@ const program = new Command(); .requiredOption('--auth-user-dir ', 'user directory for user to connect with') .requiredOption('--auth-user-id ', 'user ID for user to connect with') - .addOption(new Option('-a, --auth-type ', 'authentication type').choices(['cert']).default('cert')) + .addOption(new Option('-a, --auth-type ', 'authentication type').choices(['cert', 'jwt']).default('cert')) .option('--auth-cert-file ', 'Qlik Sense certificate file (exported from QMC)', './cert/client.pem') .option('--auth-cert-key-file ', 'Qlik Sense certificate key file (exported from QMC)', './cert/client_key.pem') .option('--auth-root-cert-file ', 'Qlik Sense root certificate file (exported from QMC)', './cert/root.pem') + .option('--auth-jwt ', 'JSON Web Token (JWT) to use for authentication with Qlik Sense server') .addOption( new Option('--id-type ', 'type of identifier passed in the --variable option').choices(['id', 'name']).default('name') @@ -392,8 +398,8 @@ const program = new Command(); new Option('--log-level ', 'log level').choices(['error', 'warn', 'info', 'verbose', 'debug', 'silly']).default('info') ) .requiredOption('--host ', 'Qlik Sense server IP/FQDN') - .option('--engine-port ', 'Qlik Sense server engine port', '4747') - .option('--qrs-port ', 'Qlik Sense repository service (QRS) port', '4242') + .option('--engine-port ', 'Qlik Sense server engine port (usually 4747 for cert auth, 443 for jwt auth)', '4747') + .option('--qrs-port ', 'Qlik Sense repository service (QRS) port (usually 4242 for cert auth, 443 for jwt auth)', '4242') .option('--schema-version ', 'Qlik Sense engine schema version', '12.612.0') .option('--app-id ', 'Qlik Sense app ID(s) to get variables from') .option('--app-tag ', 'Qlik Sense app tag(s) to get variables') @@ -402,10 +408,11 @@ const program = new Command(); .requiredOption('--auth-user-dir ', 'user directory for user to connect with') .requiredOption('--auth-user-id ', 'user ID for user to connect with') - .addOption(new Option('-a, --auth-type ', 'authentication type').choices(['cert']).default('cert')) + .addOption(new Option('-a, --auth-type ', 'authentication type').choices(['cert', 'jwt']).default('cert')) .option('--auth-cert-file ', 'Qlik Sense certificate file (exported from QMC)', './cert/client.pem') .option('--auth-cert-key-file ', 'Qlik Sense certificate key file (exported from QMC)', './cert/client_key.pem') .option('--auth-root-cert-file ', 'Qlik Sense root certificate file (exported from QMC)', './cert/root.pem') + .option('--auth-jwt ', 'JSON Web Token (JWT) to use for authentication with Qlik Sense server') .addOption( new Option('--id-type ', 'type of identifier passed in the --variable option').choices(['id', 'name']).default('name') @@ -427,7 +434,7 @@ const program = new Command(); new Option('--log-level ', 'log level').choices(['error', 'warn', 'info', 'verbose', 'debug', 'silly']).default('info') ) .requiredOption('--host ', 'Qlik Sense server IP/FQDN') - .option('--port ', 'Qlik Sense server engine port', '4747') + .option('--port ', 'Qlik Sense server engine port (usually 4747 for cert auth, 443 for jwt auth)', '4747') .option('--schema-version ', 'Qlik Sense engine schema version', '12.612.0') .requiredOption('--app-id ', 'Qlik Sense app ID') .requiredOption('--virtual-proxy ', 'Qlik Sense virtual proxy prefix', '') @@ -435,10 +442,11 @@ const program = new Command(); .requiredOption('--auth-user-dir ', 'user directory for user to connect with') .requiredOption('--auth-user-id ', 'user ID for user to connect with') - .addOption(new Option('-a, --auth-type ', 'authentication type').choices(['cert']).default('cert')) + .addOption(new Option('-a, --auth-type ', 'authentication type').choices(['cert', 'jwt']).default('cert')) .option('--auth-cert-file ', 'Qlik Sense certificate file (exported from QMC)', './cert/client.pem') .option('--auth-cert-key-file ', 'Qlik Sense certificate key file (exported from QMC)', './cert/client_key.pem') .option('--auth-root-cert-file ', 'Qlik Sense root certificate file (exported from QMC)', './cert/root.pem') + .option('--auth-jwt ', 'JSON Web Token (JWT) to use for authentication with Qlik Sense server') .requiredOption('--field-name ', 'name of field(s) to be scrambled') .requiredOption('--new-app-name ', 'name of new app that will contain scrambled data'); @@ -457,7 +465,7 @@ const program = new Command(); new Option('--log-level ', 'log level').choices(['error', 'warn', 'info', 'verbose', 'debug', 'silly']).default('info') ) .requiredOption('--host ', 'Qlik Sense server IP/FQDN') - .option('--port ', 'Qlik Sense server engine port', '4747') + .option('--port ', 'Qlik Sense server engine port (usually 4747 for cert auth, 443 for jwt auth)', '4747') .option('--schema-version ', 'Qlik Sense engine schema version', '12.612.0') .requiredOption('--app-id ', 'Qlik Sense app ID') .requiredOption('--virtual-proxy ', 'Qlik Sense virtual proxy prefix', '') @@ -465,10 +473,11 @@ const program = new Command(); .requiredOption('--auth-user-dir ', 'user directory for user to connect with') .requiredOption('--auth-user-id ', 'user ID for user to connect with') - .addOption(new Option('-a, --auth-type ', 'authentication type').choices(['cert']).default('cert')) + .addOption(new Option('-a, --auth-type ', 'authentication type').choices(['cert', 'jwt']).default('cert')) .option('--auth-cert-file ', 'Qlik Sense certificate file (exported from QMC)', './cert/client.pem') .option('--auth-cert-key-file ', 'Qlik Sense certificate key file (exported from QMC)', './cert/client_key.pem') - .option('--auth-root-cert-file ', 'Qlik Sense root certificate file (exported from QMC)', './cert/root.pem'); + .option('--auth-root-cert-file ', 'Qlik Sense root certificate file (exported from QMC)', './cert/root.pem') + .option('--auth-jwt ', 'JSON Web Token (JWT) to use for authentication with Qlik Sense server'); // Get bookmark command program @@ -484,7 +493,7 @@ const program = new Command(); new Option('--log-level ', 'log level').choices(['error', 'warn', 'info', 'verbose', 'debug', 'silly']).default('info') ) .requiredOption('--host ', 'Qlik Sense server IP/FQDN') - .option('--port ', 'Qlik Sense server engine port', '4747') + .option('--port ', 'Qlik Sense server engine port (usually 4747 for cert auth, 443 for jwt auth)', '4747') .option('--schema-version ', 'Qlik Sense engine schema version', '12.612.0') .requiredOption('--app-id ', 'Qlik Sense app ID') .requiredOption('--virtual-proxy ', 'Qlik Sense virtual proxy prefix', '') @@ -492,10 +501,11 @@ const program = new Command(); .requiredOption('--auth-user-dir ', 'user directory for user to connect with') .requiredOption('--auth-user-id ', 'user ID for user to connect with') - .addOption(new Option('-a, --auth-type ', 'authentication type').choices(['cert']).default('cert')) + .addOption(new Option('-a, --auth-type ', 'authentication type').choices(['cert', 'jwt']).default('cert')) .option('--auth-cert-file ', 'Qlik Sense certificate file (exported from QMC)', './cert/client.pem') .option('--auth-cert-key-file ', 'Qlik Sense certificate key file (exported from QMC)', './cert/client_key.pem') .option('--auth-root-cert-file ', 'Qlik Sense root certificate file (exported from QMC)', './cert/root.pem') + .option('--auth-jwt ', 'JSON Web Token (JWT) to use for authentication with Qlik Sense server') .addOption( new Option('--id-type ', 'type of bookmark identifier passed in the --bookmark option') @@ -532,17 +542,18 @@ const program = new Command(); new Option('--log-level ', 'log level').choices(['error', 'warn', 'info', 'verbose', 'debug', 'silly']).default('info') ) .requiredOption('--host ', 'Qlik Sense server IP/FQDN') - .option('--port ', 'Qlik Sense repository service (QRS) port', '4242') + .option('--port ', 'Qlik Sense repository service (QRS) port (usually 4242 for cert auth, 443 for jwt auth)', '4242') .option('--schema-version ', 'Qlik Sense engine schema version', '12.612.0') .requiredOption('--virtual-proxy ', 'Qlik Sense virtual proxy prefix', '') .requiredOption('--secure ', 'connection to Qlik Sense engine is via https', true) .requiredOption('--auth-user-dir ', 'user directory for user to connect with') .requiredOption('--auth-user-id ', 'user ID for user to connect with') - .addOption(new Option('-a, --auth-type ', 'authentication type').choices(['cert']).default('cert')) + .addOption(new Option('-a, --auth-type ', 'authentication type').choices(['cert', 'jwt']).default('cert')) .option('--auth-cert-file ', 'Qlik Sense certificate file (exported from QMC)', './cert/client.pem') .option('--auth-cert-key-file ', 'Qlik Sense certificate key file (exported from QMC)', './cert/client_key.pem') .option('--auth-root-cert-file ', 'Qlik Sense root certificate file (exported from QMC)', './cert/root.pem') + .option('--auth-jwt ', 'JSON Web Token (JWT) to use for authentication with Qlik Sense server') .addOption(new Option('--task-type ', 'type of tasks to include').choices(['reload', 'ext-program'])) .option('--task-id ', 'use task IDs to select which tasks to retrieve. Only allowed when --output-format=table') @@ -586,17 +597,18 @@ const program = new Command(); new Option('--log-level ', 'log level').choices(['error', 'warn', 'info', 'verbose', 'debug', 'silly']).default('info') ) .requiredOption('--host ', 'Qlik Sense server IP/FQDN') - .option('--port ', 'Qlik Sense repository service (QRS) port', '4242') + .option('--port ', 'Qlik Sense repository service (QRS) port (usually 4242 for cert auth, 443 for jwt auth)', '4242') .option('--schema-version ', 'Qlik Sense engine schema version', '12.612.0') .requiredOption('--virtual-proxy ', 'Qlik Sense virtual proxy prefix', '') .requiredOption('--secure ', 'connection to Qlik Sense engine is via https', true) .requiredOption('--auth-user-dir ', 'user directory for user to connect with') .requiredOption('--auth-user-id ', 'user ID for user to connect with') - .addOption(new Option('-a, --auth-type ', 'authentication type').choices(['cert']).default('cert')) + .addOption(new Option('-a, --auth-type ', 'authentication type').choices(['cert', 'jwt']).default('cert')) .option('--auth-cert-file ', 'Qlik Sense certificate file (exported from QMC)', './cert/client.pem') .option('--auth-cert-key-file ', 'Qlik Sense certificate key file (exported from QMC)', './cert/client_key.pem') .option('--auth-root-cert-file ', 'Qlik Sense root certificate file (exported from QMC)', './cert/root.pem') + .option('--auth-jwt ', 'JSON Web Token (JWT) to use for authentication with Qlik Sense server') .addOption( new Option('--task-type ', 'type of tasks to list').choices(['reload']).default(['reload']) @@ -633,17 +645,18 @@ const program = new Command(); new Option('--log-level ', 'log level').choices(['error', 'warn', 'info', 'verbose', 'debug', 'silly']).default('info') ) .requiredOption('--host ', 'Qlik Sense server IP/FQDN') - .option('--port ', 'Qlik Sense repository service (QRS) port', '4242') + .option('--port ', 'Qlik Sense repository service (QRS) port (usually 4242 for cert auth, 443 for jwt auth)', '4242') .option('--schema-version ', 'Qlik Sense engine schema version', '12.612.0') .requiredOption('--virtual-proxy ', 'Qlik Sense virtual proxy prefix', '') .requiredOption('--secure ', 'connection to Qlik Sense engine is via https', true) .requiredOption('--auth-user-dir ', 'user directory for user to connect with') .requiredOption('--auth-user-id ', 'user ID for user to connect with') - .addOption(new Option('-a, --auth-type ', 'authentication type').choices(['cert']).default('cert')) + .addOption(new Option('-a, --auth-type ', 'authentication type').choices(['cert', 'jwt']).default('cert')) .option('--auth-cert-file ', 'Qlik Sense certificate file (exported from QMC)', './cert/client.pem') .option('--auth-cert-key-file ', 'Qlik Sense certificate key file (exported from QMC)', './cert/client_key.pem') .option('--auth-root-cert-file ', 'Qlik Sense root certificate file (exported from QMC)', './cert/root.pem') + .option('--auth-jwt ', 'JSON Web Token (JWT) to use for authentication with Qlik Sense server') .addOption(new Option('-t, --file-type ', 'source file type').choices(['excel', 'csv']).default('excel')) .requiredOption('--file-name ', 'file containing task definitions') @@ -684,17 +697,18 @@ const program = new Command(); new Option('--log-level ', 'log level').choices(['error', 'warn', 'info', 'verbose', 'debug', 'silly']).default('info') ) .requiredOption('--host ', 'Qlik Sense server IP/FQDN') - .option('--port ', 'Qlik Sense repository service (QRS) port', '4242') + .option('--port ', 'Qlik Sense repository service (QRS) port (usually 4242 for cert auth, 443 for jwt auth)', '4242') .option('--schema-version ', 'Qlik Sense engine schema version', '12.612.0') .requiredOption('--virtual-proxy ', 'Qlik Sense virtual proxy prefix', '') .requiredOption('--secure ', 'connection to Qlik Sense engine is via https', true) .requiredOption('--auth-user-dir ', 'user directory for user to connect with') .requiredOption('--auth-user-id ', 'user ID for user to connect with') - .addOption(new Option('-a, --auth-type ', 'authentication type').choices(['cert']).default('cert')) + .addOption(new Option('-a, --auth-type ', 'authentication type').choices(['cert', 'jwt']).default('cert')) .option('--auth-cert-file ', 'Qlik Sense certificate file (exported from QMC)', './cert/client.pem') .option('--auth-cert-key-file ', 'Qlik Sense certificate key file (exported from QMC)', './cert/client_key.pem') .option('--auth-root-cert-file ', 'Qlik Sense root certificate file (exported from QMC)', './cert/root.pem') + .option('--auth-jwt ', 'JSON Web Token (JWT) to use for authentication with Qlik Sense server') .addOption(new Option('-t, --file-type ', 'source file type').choices(['excel']).default('excel')) .requiredOption('--file-name ', 'file containing app definitions') @@ -726,17 +740,18 @@ const program = new Command(); new Option('--log-level ', 'log level').choices(['error', 'warn', 'info', 'verbose', 'debug', 'silly']).default('info') ) .requiredOption('--host ', 'Qlik Sense server IP/FQDN') - .option('--port ', 'Qlik Sense repository service (QRS) port', '4242') + .option('--port ', 'Qlik Sense repository service (QRS) port (usually 4242 for cert auth, 443 for jwt auth)', '4242') .option('--schema-version ', 'Qlik Sense engine schema version', '12.612.0') .requiredOption('--virtual-proxy ', 'Qlik Sense virtual proxy prefix', '') .requiredOption('--secure ', 'connection to Qlik Sense engine is via https', true) .requiredOption('--auth-user-dir ', 'user directory for user to connect with') .requiredOption('--auth-user-id ', 'user ID for user to connect with') - .addOption(new Option('-a, --auth-type ', 'authentication type').choices(['cert']).default('cert')) + .addOption(new Option('-a, --auth-type ', 'authentication type').choices(['cert', 'jwt']).default('cert')) .option('--auth-cert-file ', 'Qlik Sense certificate file (exported from QMC)', './cert/client.pem') .option('--auth-cert-key-file ', 'Qlik Sense certificate key file (exported from QMC)', './cert/client_key.pem') .option('--auth-root-cert-file ', 'Qlik Sense root certificate file (exported from QMC)', './cert/root.pem') + .option('--auth-jwt ', 'JSON Web Token (JWT) to use for authentication with Qlik Sense server') .option('--app-id ', 'use app IDs to select which apps to export') .option('--app-tag ', 'use app tags to select which apps to export') @@ -786,7 +801,7 @@ const program = new Command(); new Option('--log-level ', 'log level').choices(['error', 'warn', 'info', 'verbose', 'debug', 'silly']).default('info') ) .requiredOption('--host ', 'Qlik Sense server IP/FQDN') - .option('--port ', 'Qlik Sense repository service (QRS) port', '4242') + .option('--port ', 'Qlik Sense proxy service port', '4242') .option('--schema-version ', 'Qlik Sense engine schema version', '12.612.0') .requiredOption('--virtual-proxy ', 'Qlik Sense virtual proxy prefix', '') .requiredOption('--secure ', 'connection to Qlik Sense engine is via https', true) diff --git a/src/lib/app/class_allapps.js b/src/lib/app/class_allapps.js index 50ce93b..548ccf5 100644 --- a/src/lib/app/class_allapps.js +++ b/src/lib/app/class_allapps.js @@ -26,9 +26,12 @@ class QlikSenseApps { this.appList = []; this.options = options; - // Make sure certificates exist - this.fileCert = path.resolve(execPath, options.authCertFile); - this.fileCertKey = path.resolve(execPath, options.authCertKeyFile); + // Should cerrificates be used for authentication? + if (options.authType === 'cert') { + // Make sure certificates exist + this.fileCert = path.resolve(execPath, options.authCertFile); + this.fileCertKey = path.resolve(execPath, options.authCertKeyFile); + } // Map that will connect app counter from Excel file with ID an app gets after import to QSEoW this.appCounterIdMap = new Map(); @@ -107,22 +110,38 @@ class QlikSenseApps { } logger.debug(`GET APPS FROM QSEOW: QRS query filter (incl ids, tags): ${filter}`); + // Should cerrificates be used for authentication? let axiosConfig; - if (filter === '') { - axiosConfig = await setupQRSConnection(this.options, { - method: 'get', - fileCert: this.fileCert, - fileCertKey: this.fileCertKey, - path: '/qrs/app/full', - }); - } else { - axiosConfig = await setupQRSConnection(this.options, { - method: 'get', - fileCert: this.fileCert, - fileCertKey: this.fileCertKey, - path: '/qrs/app/full', - queryParameters: [{ name: 'filter', value: filter }], - }); + if (this.options.authType === 'cert') { + if (filter === '') { + axiosConfig = await setupQRSConnection(this.options, { + method: 'get', + fileCert: this.fileCert, + fileCertKey: this.fileCertKey, + path: '/qrs/app/full', + }); + } else { + axiosConfig = await setupQRSConnection(this.options, { + method: 'get', + fileCert: this.fileCert, + fileCertKey: this.fileCertKey, + path: '/qrs/app/full', + queryParameters: [{ name: 'filter', value: filter }], + }); + } + } else if (this.options.authType === 'jwt') { + if (filter === '') { + axiosConfig = await setupQRSConnection(this.options, { + method: 'get', + path: '/qrs/app/full', + }); + } else { + axiosConfig = await setupQRSConnection(this.options, { + method: 'get', + path: '/qrs/app/full', + queryParameters: [{ name: 'filter', value: filter }], + }); + } } const result = await axios.request(axiosConfig); @@ -343,6 +362,9 @@ class QlikSenseApps { process.exit(1); } + // Save id of created app + currentApp.createdAppId = uploadedAppId; + // Update tags, custom properties and owner of uploaded app // eslint-disable-next-line no-await-in-loop const result = await this.updateUploadedApp(currentApp, uploadedAppId); @@ -353,6 +375,7 @@ class QlikSenseApps { // eslint-disable-next-line no-await-in-loop const { streamId, streamName } = await this.getStreamInfo(currentApp); + let tmpAppId; // Do we know which stream to publish to? Publish if so! if (streamId) { @@ -374,19 +397,29 @@ class QlikSenseApps { }` ); + // Keep record of published app + currentApp.publishStatus = 'published'; + // Add mapping between app counter and the id of published app - const tmpAppId = `newapp-${currentApp.appCounter}`; + tmpAppId = `newapp-${currentApp.appCounter}`; this.appCounterIdMap.set(tmpAppId, result2.publishedApp.id); - - // eslint-disable-next-line no-await-in-loop - await this.addApp(currentApp, tmpAppId); } else { logger.error( `(${appRow[0][appFileColumnHeaders.appCounter.pos]}) Failed publishing app "${ currentApp.name }" to stream "${streamName}"` ); + + // Keep record of failed app + currentApp.publishStatus = 'failed'; + + // Add mapping between app counter and the id of uploaded, not published app + tmpAppId = `newapp-${currentApp.appCounter}`; + this.appCounterIdMap.set(tmpAppId, uploadedAppId); } + + // eslint-disable-next-line no-await-in-loop + await this.addApp(currentApp, tmpAppId); } else if (currentApp.appPublishToStreamOption === 'publish-another') { // eslint-disable-next-line no-await-in-loop const result2 = await this.streamAppPublishAnother( @@ -403,19 +436,29 @@ class QlikSenseApps { }" published to stream "${streamName}". Id of published app: ${result2.publishedApp.id}` ); + // Keep record of published app + currentApp.publishStatus = 'published'; + // Add mapping between app counter and the id of published app - const tmpAppId = `newapp-${currentApp.appCounter}`; + tmpAppId = `newapp-${currentApp.appCounter}`; this.appCounterIdMap.set(tmpAppId, result2.publishedApp.id); - - // eslint-disable-next-line no-await-in-loop - await this.addApp(currentApp, tmpAppId); } else { logger.error( `(${appRow[0][appFileColumnHeaders.appCounter.pos]}) Failed publishing app "${ currentApp.name }" to stream "${streamName}"` ); + + // Keep record of failed app + currentApp.publishStatus = 'failed'; + + // Add mapping between app counter and the id of uploaded, not published app + tmpAppId = `newapp-${currentApp.appCounter}`; + this.appCounterIdMap.set(tmpAppId, uploadedAppId); } + + // eslint-disable-next-line no-await-in-loop + await this.addApp(currentApp, tmpAppId); } else if (currentApp.appPublishToStreamOption === 'delete-publish') { // eslint-disable-next-line no-await-in-loop const result2 = await this.streamAppDeletePublish( @@ -435,18 +478,29 @@ class QlikSenseApps { }` ); + // Keep record of published app + currentApp.publishStatus = 'published'; + // Add mapping between app counter and the id of published app - const tmpAppId = `newapp-${currentApp.appCounter}`; + tmpAppId = `newapp-${currentApp.appCounter}`; this.appCounterIdMap.set(tmpAppId, result2.publishedApp.id); - - // eslint-disable-next-line no-await-in-loop - await this.addApp(currentApp, tmpAppId); - } else + } else { logger.error( `(${appRow[0][appFileColumnHeaders.appCounter.pos]}) Failed publishing app "${ currentApp.name }" to stream "${streamName}"` ); + + // Keep record of failed app + currentApp.publishStatus = 'failed'; + + // Add mapping between app counter and the id of uploaded, not published app + tmpAppId = `newapp-${currentApp.appCounter}`; + this.appCounterIdMap.set(tmpAppId, uploadedAppId); + } + + // eslint-disable-next-line no-await-in-loop + await this.addApp(currentApp, tmpAppId); } else { logger.error( `(${appRow[0][appFileColumnHeaders.appCounter.pos]}) Invalid publish option specified for app "${ @@ -462,6 +516,16 @@ class QlikSenseApps { currentApp.appPublishToStream }". The uploaded app is still present in the QMC (id=${uploadedAppId}).` ); + + // Keep record of failed app + currentApp.publishStatus = 'failed'; + + // Add mapping between app counter and the id of uploaded, but not published app + const tmpAppId = `newapp-${currentApp.appCounter}`; + this.appCounterIdMap.set(tmpAppId, uploadedAppId); + + // eslint-disable-next-line no-await-in-loop + await this.addApp(currentApp, tmpAppId); } } else { // No, do not publish to stream after app upload @@ -471,6 +535,9 @@ class QlikSenseApps { }" uploaded to QSEoW, but not published to any stream.` ); + // Keep record of publish status + currentApp.publishStatus = 'unpublished'; + // Add mapping between app counter and the new id of imported app const tmpAppId = `newapp-${currentApp.appCounter}`; this.appCounterIdMap.set(tmpAppId, uploadedAppId); @@ -491,13 +558,22 @@ class QlikSenseApps { // Function to update tags, custom properties and owner of uploaded app async updateUploadedApp(newApp, uploadedAppId) { try { - // Get info about just uploaded app - const axiosConfigUploadedApp = setupQRSConnection(this.options, { - method: 'get', - fileCert: this.fileCert, - fileCertKey: this.fileCertKey, - path: `/qrs/app/${uploadedAppId}`, - }); + // Should cerrificates be used for authentication? + let axiosConfigUploadedApp; + if (this.options.authType === 'cert') { + // Get info about just uploaded app + axiosConfigUploadedApp = setupQRSConnection(this.options, { + method: 'get', + fileCert: this.fileCert, + fileCertKey: this.fileCertKey, + path: `/qrs/app/${uploadedAppId}`, + }); + } else if (this.options.authType === 'jwt') { + axiosConfigUploadedApp = setupQRSConnection(this.options, { + method: 'get', + path: `/qrs/app/${uploadedAppId}`, + }); + } const appUploaded2 = await axios.request(axiosConfigUploadedApp); if (appUploaded2.status !== 200) { @@ -523,13 +599,24 @@ class QlikSenseApps { `userDirectory eq '${newApp.appOwnerUserDirectory}' and userId eq '${newApp.appOwnerUserId}'` ); - const axiosConfigUser = setupQRSConnection(this.options, { - method: 'get', - fileCert: this.fileCert, - fileCertKey: this.fileCertKey, - path: '/qrs/user', - queryParameters: [{ name: 'filter', value: filter }], - }); + // Should cerrificates be used for authentication? + let axiosConfigUser; + if (this.options.authType === 'cert') { + // Get info about just uploaded app + axiosConfigUser = setupQRSConnection(this.options, { + method: 'get', + fileCert: this.fileCert, + fileCertKey: this.fileCertKey, + path: '/qrs/user', + queryParameters: [{ name: 'filter', value: filter }], + }); + } else if (this.options.authType === 'jwt') { + axiosConfigUser = setupQRSConnection(this.options, { + method: 'get', + path: '/qrs/user', + queryParameters: [{ name: 'filter', value: filter }], + }); + } const userResult = await axios.request(axiosConfigUser); const userResponse = JSON.parse(userResult.data); @@ -559,17 +646,28 @@ class QlikSenseApps { // Pause for a while to let Sense repository catch up await sleep(1000); - - // Uppdate app with tags, custom properties and app owner - const axiosConfig2 = setupQRSConnection(this.options, { - method: 'put', - fileCert: this.fileCert, - fileCertKey: this.fileCertKey, - path: `/qrs/app/${app.id}`, - body: app, - }); - + // console.log(this.options) + // Should cerrificates be used for authentication? + let axiosConfig2; + if (this.options.authType === 'cert') { + // Uppdate app with tags, custom properties and app owner + axiosConfig2 = setupQRSConnection(this.options, { + method: 'put', + fileCert: this.fileCert, + fileCertKey: this.fileCertKey, + path: `/qrs/app/${app.id}`, + body: app, + }); + } else if (this.options.authType === 'jwt') { + axiosConfig2 = setupQRSConnection(this.options, { + method: 'put', + path: `/qrs/app/${app.id}`, + body: app, + }); + } + // console.log(axiosConfig2) const result2 = await axios.request(axiosConfig2); + // console.log('b1') if (result2.status === 200) { logger.debug(`Update of imported app wrt tags, custom properties and owner was successful.`); return true; @@ -600,7 +698,7 @@ class QlikSenseApps { logger.debug(`(${appCounter}) PUBLISH APP publish-replace: Starting`); // Get info about the created app - const appInfo = await getAppById(uploadedAppId); + const appInfo = await getAppById(uploadedAppId, this.options); // Check if there is an app with the same name in the target stream const matchingAppsInStream = await this.appsInStreamCount(appInfo.name, streamName); @@ -612,7 +710,7 @@ class QlikSenseApps { if (result.res === true) { // Get info about the published app - result.publishedApp = await getAppById(uploadedAppId); + result.publishedApp = await getAppById(uploadedAppId, this.options); } else { result.publishedApp = null; } @@ -644,10 +742,10 @@ class QlikSenseApps { // ); // Delete the uploaded app - await deleteAppById(uploadedAppId); + await deleteAppById(uploadedAppId, this.options); // } - const publishedApp = await getAppById(appInStream.id); + const publishedApp = await getAppById(appInStream.id, this.options); result = { res: true, publishedApp }; } else { // Something went wrong @@ -692,7 +790,7 @@ class QlikSenseApps { if (result.res === true) { // Get info about the published app - result.publishedApp = await getAppById(uploadedAppId); + result.publishedApp = await getAppById(uploadedAppId, this.options); } else { result.publishedApp = null; } @@ -727,7 +825,7 @@ class QlikSenseApps { if (result.res === true) { // Get info about the published app - result.publishedApp = await getAppById(uploadedAppId); + result.publishedApp = await getAppById(uploadedAppId, this.options); } else { result.publishedApp = null; } @@ -747,14 +845,14 @@ class QlikSenseApps { const appInStream = await this.getAppInStream(streamName, appName); // Delete the app in the target stream - await deleteAppById(appInStream.id); + await deleteAppById(appInStream.id, this.options); // Do the normal publish result.res = await this.appPublishNormal(streamId, uploadedAppId, appName); if (result.res === true) { // Get info about the published app - result.publishedApp = await getAppById(uploadedAppId); + result.publishedApp = await getAppById(uploadedAppId, this.options); } else { result.publishedApp = null; } @@ -790,14 +888,24 @@ class QlikSenseApps { { name: 'name', value: appName }, ]; - // Build QRS query - const axiosConfig = setupQRSConnection(this.options, { - method: 'put', - fileCert: this.fileCert, - fileCertKey: this.fileCertKey, - path: `/qrs/app/${appId}/publish`, - queryParameters, - }); + // Should cerrificates be used for authentication? + let axiosConfig; + if (this.options.authType === 'cert') { + // Build QRS query + axiosConfig = setupQRSConnection(this.options, { + method: 'put', + fileCert: this.fileCert, + fileCertKey: this.fileCertKey, + path: `/qrs/app/${appId}/publish`, + queryParameters, + }); + } else if (this.options.authType === 'jwt') { + axiosConfig = setupQRSConnection(this.options, { + method: 'put', + path: `/qrs/app/${appId}/publish`, + queryParameters, + }); + } // Execute QRS query const result = await axios.request(axiosConfig); @@ -830,14 +938,24 @@ class QlikSenseApps { // Define query parameters const queryParameters = [{ name: 'app', value: targetAppId }]; - // Build QRS query - const axiosConfig = setupQRSConnection(this.options, { - method: 'put', - fileCert: this.fileCert, - fileCertKey: this.fileCertKey, - path: `/qrs/app/${sourceAppId}/replace`, - queryParameters, - }); + // Should cerrificates be used for authentication? + let axiosConfig; + if (this.options.authType === 'cert') { + // Build QRS query + axiosConfig = setupQRSConnection(this.options, { + method: 'put', + fileCert: this.fileCert, + fileCertKey: this.fileCertKey, + path: `/qrs/app/${sourceAppId}/replace`, + queryParameters, + }); + } else if (this.options.authType === 'jwt') { + axiosConfig = setupQRSConnection(this.options, { + method: 'put', + path: `/qrs/app/${sourceAppId}/replace`, + queryParameters, + }); + } // Execute QRS query const result = await axios.request(axiosConfig); @@ -885,13 +1003,24 @@ class QlikSenseApps { filter = encodeURIComponent(`stream.name eq '${streamName}' and name eq '${appName}'`); } - const axiosConfig = setupQRSConnection(this.options, { - method: 'get', - fileCert: this.fileCert, - fileCertKey: this.fileCertKey, - path: `/qrs/app`, - queryParameters: [{ name: 'filter', value: filter }], - }); + // Should cerrificates be used for authentication? + let axiosConfig; + if (this.options.authType === 'cert') { + // Build QRS query + axiosConfig = setupQRSConnection(this.options, { + method: 'get', + fileCert: this.fileCert, + fileCertKey: this.fileCertKey, + path: `/qrs/app`, + queryParameters: [{ name: 'filter', value: filter }], + }); + } else if (this.options.authType === 'jwt') { + axiosConfig = setupQRSConnection(this.options, { + method: 'get', + path: `/qrs/app`, + queryParameters: [{ name: 'filter', value: filter }], + }); + } // Execute QRS query const result = await axios.request(axiosConfig); @@ -925,13 +1054,24 @@ class QlikSenseApps { // Build QRS query const filter = encodeURIComponent(`stream.name eq '${streamName}' and name eq '${appName}'`); - const axiosConfig = setupQRSConnection(this.options, { - method: 'get', - fileCert: this.fileCert, - fileCertKey: this.fileCertKey, - path: `/qrs/app`, - queryParameters: [{ name: 'filter', value: filter }], - }); + // Should cerrificates be used for authentication? + let axiosConfig; + if (this.options.authType === 'cert') { + // Build QRS query + axiosConfig = setupQRSConnection(this.options, { + method: 'get', + fileCert: this.fileCert, + fileCertKey: this.fileCertKey, + path: `/qrs/app`, + queryParameters: [{ name: 'filter', value: filter }], + }); + } else if (this.options.authType === 'jwt') { + axiosConfig = setupQRSConnection(this.options, { + method: 'get', + path: `/qrs/app`, + queryParameters: [{ name: 'filter', value: filter }], + }); + } // Execute QRS query const result = await axios.request(axiosConfig); @@ -981,12 +1121,22 @@ class QlikSenseApps { // If so check if the GUID represents a stream if (validate(uploadedAppInfo.appPublishToStream)) { // It's a valid GUID - axiosConfigPublish = setupQRSConnection(this.options, { - method: 'get', - fileCert: this.fileCert, - fileCertKey: this.fileCertKey, - path: `/qrs/stream/${uploadedAppInfo.appPublishToStream}`, - }); + + // Should cerrificates be used for authentication? + if (this.options.authType === 'cert') { + // Build QRS query + axiosConfigPublish = setupQRSConnection(this.options, { + method: 'get', + fileCert: this.fileCert, + fileCertKey: this.fileCertKey, + path: `/qrs/stream/${uploadedAppInfo.appPublishToStream}`, + }); + } else if (this.options.authType === 'jwt') { + axiosConfigPublish = setupQRSConnection(this.options, { + method: 'get', + path: `/qrs/stream/${uploadedAppInfo.appPublishToStream}`, + }); + } resultPublish = await axios.request(axiosConfigPublish); if (resultPublish.status === 200) { @@ -1000,13 +1150,23 @@ class QlikSenseApps { // Provided stream name is not a GUID, make sure only one stream exists with this name, then get its GUID const filter = encodeURIComponent(`name eq '${uploadedAppInfo.appPublishToStream}'`); - axiosConfigPublish = setupQRSConnection(this.options, { - method: 'get', - fileCert: this.fileCert, - fileCertKey: this.fileCertKey, - path: '/qrs/stream', - queryParameters: [{ name: 'filter', value: filter }], - }); + // Should cerrificates be used for authentication? + if (this.options.authType === 'cert') { + // Build QRS query + axiosConfigPublish = setupQRSConnection(this.options, { + method: 'get', + fileCert: this.fileCert, + fileCertKey: this.fileCertKey, + path: '/qrs/stream', + queryParameters: [{ name: 'filter', value: filter }], + }); + } else if (this.options.authType === 'jwt') { + axiosConfigPublish = setupQRSConnection(this.options, { + method: 'get', + path: '/qrs/stream', + queryParameters: [{ name: 'filter', value: filter }], + }); + } resultPublish = await axios.request(axiosConfigPublish); if (resultPublish.status === 200) { @@ -1159,13 +1319,24 @@ class QlikSenseApps { const exportToken = uuidv4(); const excludeData = this.options.excludeAppData === 'true' ? 'true' : 'false'; - const axiosConfig = setupQRSConnection(this.options, { - method: 'post', - fileCert: this.fileCert, - fileCertKey: this.fileCertKey, - path: `/qrs/app/${app.id}/export/${exportToken}`, - queryParameters: [{ name: 'skipData', value: excludeData }], - }); + // Should cerrificates be used for authentication? + let axiosConfig; + if (this.options.authType === 'cert') { + // Build QRS query + axiosConfig = setupQRSConnection(this.options, { + method: 'post', + fileCert: this.fileCert, + fileCertKey: this.fileCertKey, + path: `/qrs/app/${app.id}/export/${exportToken}`, + queryParameters: [{ name: 'skipData', value: excludeData }], + }); + } else if (this.options.authType === 'jwt') { + axiosConfig = setupQRSConnection(this.options, { + method: 'post', + path: `/qrs/app/${app.id}/export/${exportToken}`, + queryParameters: [{ name: 'skipData', value: excludeData }], + }); + } const result = await axios.request(axiosConfig); logger.verbose(`Export app step 1 result: [${result.status}] ${result.statusText}`); @@ -1264,13 +1435,24 @@ class QlikSenseApps { } else { writer = fs2.createWriteStream(fileName); - const axiosConfig = setupQRSConnection(this.options, { - method: 'get', - fileCert: this.fileCert, - fileCertKey: this.fileCertKey, - path: urlPath, - queryParameters: [{ name: paramName, value: paramValue }], - }); + // Should cerrificates be used for authentication? + let axiosConfig; + if (this.options.authType === 'cert') { + // Build QRS query + axiosConfig = setupQRSConnection(this.options, { + method: 'get', + fileCert: this.fileCert, + fileCertKey: this.fileCertKey, + path: urlPath, + queryParameters: [{ name: paramName, value: paramValue }], + }); + } else if (this.options.authType === 'jwt') { + axiosConfig = setupQRSConnection(this.options, { + method: 'get', + path: urlPath, + queryParameters: [{ name: paramName, value: paramValue }], + }); + } axiosConfig.responseType = 'stream'; diff --git a/src/lib/cmd/deletedim.js b/src/lib/cmd/deletedim.js index 10783cc..d71c68f 100644 --- a/src/lib/cmd/deletedim.js +++ b/src/lib/cmd/deletedim.js @@ -1,6 +1,6 @@ const enigma = require('enigma.js'); -const { setupEnigmaConnection } = require('../util/enigma'); +const { setupEnigmaConnection, addTrafficLogging } = require('../util/enigma'); const { logger, setLoggingLevel, isPkg, execPath } = require('../../globals'); // Variable to keep track of how many dimensions have been deleted @@ -21,18 +21,40 @@ const deleteMasterDimension = async (options) => { logger.info('Delete master dimensions'); logger.debug(`Options: ${JSON.stringify(options, null, 2)}`); - // Configure Enigma.js - const configEnigma = await setupEnigmaConnection(options); + // Session ID to use when connecting to the Qlik Sense server + const sessionId = 'ctrlq'; + + // Create new session to Sense engine + let configEnigma; + let session; + try { + configEnigma = await setupEnigmaConnection(options, sessionId); + session = await enigma.create(configEnigma); + logger.verbose(`Created session to server ${options.host}.`); + } catch (err) { + logger.error(`Error creating session to server ${options.host}: ${err}`); + process.exit(1); + } + + // Set up logging of websocket traffic + addTrafficLogging(session, options); - const session = enigma.create(configEnigma); - if (options.logLevel === 'silly') { - session.on('traffic:sent', (data) => console.log('sent:', data)); - session.on('traffic:received', (data) => console.log('received:', data)); + let global; + try { + global = await session.open(); + } catch (err) { + logger.error(`Error opening session to server ${options.host}: ${err}`); + process.exit(1); } - const global = await session.open(); - const engineVersion = await global.engineVersion(); - logger.verbose(`Created session to server ${options.host}, engine version is ${engineVersion.qComponentVersion}.`); + let engineVersion; + try { + engineVersion = await global.engineVersion(); + logger.verbose(`Server ${options.host} has engine version ${engineVersion.qComponentVersion}.`); + } catch (err) { + logger.error(`Error getting engine version from server ${options.host}: ${err}`); + process.exit(1); + } const app = await global.openDoc(options.appId, '', '', '', false); logger.verbose(`Opened app ${options.appId}.`); diff --git a/src/lib/cmd/deletemeasure.js b/src/lib/cmd/deletemeasure.js index 3fe22b9..f0507c9 100644 --- a/src/lib/cmd/deletemeasure.js +++ b/src/lib/cmd/deletemeasure.js @@ -1,6 +1,6 @@ const enigma = require('enigma.js'); -const { setupEnigmaConnection } = require('../util/enigma'); +const { setupEnigmaConnection, addTrafficLogging } = require('../util/enigma'); const { logger, setLoggingLevel, isPkg, execPath } = require('../../globals'); // Variable to keep track of how many measures have been deleted @@ -21,20 +21,40 @@ const deleteMasterMeasure = async (options) => { logger.info('Delete master measures'); logger.debug(`Options: ${JSON.stringify(options, null, 2)}`); - // Configure Enigma.js - const configEnigma = await setupEnigmaConnection(options); + // Session ID to use when connecting to the Qlik Sense server + const sessionId = 'ctrlq'; + + // Create new session to Sense engine + let configEnigma; + let session; + try { + configEnigma = await setupEnigmaConnection(options, sessionId); + session = await enigma.create(configEnigma); + logger.verbose(`Created session to server ${options.host}.`); + } catch (err) { + logger.error(`Error creating session to server ${options.host}: ${err}`); + process.exit(1); + } + + // Set up logging of websocket traffic + addTrafficLogging(session, options); - const session = enigma.create(configEnigma); - if (options.logLevel === 'silly') { - // eslint-disable-next-line no-console - session.on('traffic:sent', (data) => console.log('sent:', data)); - // eslint-disable-next-line no-console - session.on('traffic:received', (data) => console.log('received:', data)); + let global; + try { + global = await session.open(); + } catch (err) { + logger.error(`Error opening session to server ${options.host}: ${err}`); + process.exit(1); } - const global = await session.open(); - const engineVersion = await global.engineVersion(); - logger.verbose(`Created session to server ${options.host}, engine version is ${engineVersion.qComponentVersion}.`); + let engineVersion; + try { + engineVersion = await global.engineVersion(); + logger.verbose(`Server ${options.host} has engine version ${engineVersion.qComponentVersion}.`); + } catch (err) { + logger.error(`Error getting engine version from server ${options.host}: ${err}`); + process.exit(1); + } const app = await global.openDoc(options.appId, '', '', '', false); logger.verbose(`Opened app ${options.appId}.`); diff --git a/src/lib/cmd/deletevariable.js b/src/lib/cmd/deletevariable.js index c0e40d1..626801d 100644 --- a/src/lib/cmd/deletevariable.js +++ b/src/lib/cmd/deletevariable.js @@ -2,7 +2,7 @@ /* eslint-disable no-await-in-loop */ const enigma = require('enigma.js'); -const { setupEnigmaConnection } = require('../util/enigma'); +const { setupEnigmaConnection, addTrafficLogging } = require('../util/enigma'); const { getApps } = require('../util/app'); const { logger, setLoggingLevel, isPkg, execPath } = require('../../globals'); @@ -22,23 +22,45 @@ const deleteVariable = async (options) => { // Get IDs of all apps that should be processed const apps = await getApps(options, options.appId, options.appTag); - // Configure Enigma.js - const configEnigma = await setupEnigmaConnection(options); + // Session ID to use when connecting to the Qlik Sense server + const sessionId = 'ctrlq'; + + // Create new session to Sense engine + let configEnigma; + let session; + try { + configEnigma = await setupEnigmaConnection(options, sessionId); + session = await enigma.create(configEnigma); + logger.verbose(`Created session to server ${options.host}.`); + } catch (err) { + logger.error(`Error creating session to server ${options.host}: ${err}`); + process.exit(1); + } + + // Set up logging of websocket traffic + addTrafficLogging(session, options); + + let global; + try { + global = await session.open(); + } catch (err) { + logger.error(`Error opening session to server ${options.host}: ${err}`); + process.exit(1); + } + + let engineVersion; + try { + engineVersion = await global.engineVersion(); + logger.verbose(`Server ${options.host} has engine version ${engineVersion.qComponentVersion}.`); + } catch (err) { + logger.error(`Error getting engine version from server ${options.host}: ${err}`); + process.exit(1); + } for (const app of apps) { logger.info('------------------------'); logger.info(`Deleting variables in app ${app.id} "${app.name}"`); - const session = enigma.create(configEnigma); - if (options.logLevel === 'silly') { - session.on('traffic:sent', (data) => console.log('sent:', data)); - session.on('traffic:received', (data) => console.log('received:', data)); - } - const global = await session.open(); - - const engineVersion = await global.engineVersion(); - logger.verbose(`Created session to server ${options.host}, engine version is ${engineVersion.qComponentVersion}.`); - const doc = await global.openDoc(app.id, '', '', '', true); logger.verbose(`Opened app ${app.id} "${app.name}".`); @@ -129,8 +151,12 @@ const deleteVariable = async (options) => { } } } - // Close app session - await doc.session.close(); + } + + if ((await session.close()) === true) { + logger.verbose(`Closed session after getting master item measures in app ${options.appId} on host ${options.host}`); + } else { + logger.error(`Error closing session for app ${options.appId} on host ${options.host}`); } } catch (err) { logger.error(`DELETE VARIABLE: ${err.stack}`); diff --git a/src/lib/cmd/getbookmark.js b/src/lib/cmd/getbookmark.js index 3d1fd5b..f490cf5 100644 --- a/src/lib/cmd/getbookmark.js +++ b/src/lib/cmd/getbookmark.js @@ -1,7 +1,7 @@ const enigma = require('enigma.js'); const { table } = require('table'); -const { setupEnigmaConnection } = require('../util/enigma'); +const { setupEnigmaConnection, addTrafficLogging } = require('../util/enigma'); const { logger, setLoggingLevel, isPkg, execPath } = require('../../globals'); const consoleTableConfig = { @@ -48,20 +48,40 @@ const getBookmark = async (options) => { logger.info('Get bookmarks'); logger.debug(`Options: ${JSON.stringify(options, null, 2)}`); - // Configure Enigma.js - const configEnigma = await setupEnigmaConnection(options); + // Session ID to use when connecting to the Qlik Sense server + const sessionId = 'ctrlq'; + + // Create new session to Sense engine + let configEnigma; + let session; + try { + configEnigma = await setupEnigmaConnection(options, sessionId); + session = await enigma.create(configEnigma); + logger.verbose(`Created session to server ${options.host}.`); + } catch (err) { + logger.error(`Error creating session to server ${options.host}: ${err}`); + process.exit(1); + } + + // Set up logging of websocket traffic + addTrafficLogging(session, options); - const session = enigma.create(configEnigma); - if (options.logLevel === 'silly') { - // eslint-disable-next-line no-console - session.on('traffic:sent', (data) => console.log('sent:', data)); - // eslint-disable-next-line no-console - session.on('traffic:received', (data) => console.log('received:', data)); + let global; + try { + global = await session.open(); + } catch (err) { + logger.error(`Error opening session to server ${options.host}: ${err}`); + process.exit(1); } - const global = await session.open(); - const engineVersion = await global.engineVersion(); - logger.verbose(`Created session to server ${options.host}, engine version is ${engineVersion.qComponentVersion}.`); + let engineVersion; + try { + engineVersion = await global.engineVersion(); + logger.verbose(`Server ${options.host} has engine version ${engineVersion.qComponentVersion}.`); + } catch (err) { + logger.error(`Error getting engine version from server ${options.host}: ${err}`); + process.exit(1); + } const app = await global.openDoc(options.appId, '', '', '', false); logger.verbose(`Opened app ${options.appId}.`); diff --git a/src/lib/cmd/getdim.js b/src/lib/cmd/getdim.js index a66447a..c55b2dd 100644 --- a/src/lib/cmd/getdim.js +++ b/src/lib/cmd/getdim.js @@ -3,7 +3,7 @@ const enigma = require('enigma.js'); const { table } = require('table'); -const { setupEnigmaConnection } = require('../util/enigma'); +const { setupEnigmaConnection, addTrafficLogging } = require('../util/enigma'); const { logger, setLoggingLevel, isPkg, execPath } = require('../../globals'); const consoleTableConfig = { @@ -51,20 +51,40 @@ const getMasterDimension = async (options) => { logger.info('Get master dimensions'); logger.debug(`Options: ${JSON.stringify(options, null, 2)}`); - // Configure Enigma.js - const configEnigma = await setupEnigmaConnection(options); + // Session ID to use when connecting to the Qlik Sense server + const sessionId = 'ctrlq'; + + // Create new session to Sense engine + let configEnigma; + let session; + try { + configEnigma = await setupEnigmaConnection(options, sessionId); + session = await enigma.create(configEnigma); + logger.verbose(`Created session to server ${options.host}.`); + } catch (err) { + logger.error(`Error creating session to server ${options.host}: ${err}`); + process.exit(1); + } + + // Set up logging of websocket traffic + addTrafficLogging(session, options); - const session = enigma.create(configEnigma); - if (options.logLevel === 'silly') { - // eslint-disable-next-line no-console - session.on('traffic:sent', (data) => console.log('sent:', data)); - // eslint-disable-next-line no-console - session.on('traffic:received', (data) => console.log('received:', data)); + let global; + try { + global = await session.open(); + } catch (err) { + logger.error(`Error opening session to server ${options.host}: ${err}`); + process.exit(1); } - const global = await session.open(); - const engineVersion = await global.engineVersion(); - logger.verbose(`Created session to server ${options.host}, engine version is ${engineVersion.qComponentVersion}.`); + let engineVersion; + try { + engineVersion = await global.engineVersion(); + logger.verbose(`Server ${options.host} has engine version ${engineVersion.qComponentVersion}.`); + } catch (err) { + logger.error(`Error getting engine version from server ${options.host}: ${err}`); + process.exit(1); + } const app = await global.openDoc(options.appId, '', '', '', false); logger.verbose(`Opened app ${options.appId}.`); diff --git a/src/lib/cmd/getmeasure.js b/src/lib/cmd/getmeasure.js index 1d5bfeb..14a75dc 100644 --- a/src/lib/cmd/getmeasure.js +++ b/src/lib/cmd/getmeasure.js @@ -2,7 +2,7 @@ const enigma = require('enigma.js'); const { table } = require('table'); -const { setupEnigmaConnection } = require('../util/enigma'); +const { setupEnigmaConnection, addTrafficLogging } = require('../util/enigma'); const { logger, setLoggingLevel, isPkg, execPath } = require('../../globals'); const consoleTableConfig = { @@ -49,20 +49,40 @@ const getMasterMeasure = async (options) => { logger.info('Get master measures'); logger.debug(`Options: ${JSON.stringify(options, null, 2)}`); - // Configure Enigma.js - const configEnigma = await setupEnigmaConnection(options); + // Session ID to use when connecting to the Qlik Sense server + const sessionId = 'ctrlq'; + + // Create new session to Sense engine + let configEnigma; + let session; + try { + configEnigma = await setupEnigmaConnection(options, sessionId); + session = await enigma.create(configEnigma); + logger.verbose(`Created session to server ${options.host}.`); + } catch (err) { + logger.error(`Error creating session to server ${options.host}: ${err}`); + process.exit(1); + } + + // Set up logging of websocket traffic + addTrafficLogging(session, options); - const session = enigma.create(configEnigma); - if (options.logLevel === 'silly') { - // eslint-disable-next-line no-console - session.on('traffic:sent', (data) => console.log('sent:', data)); - // eslint-disable-next-line no-console - session.on('traffic:received', (data) => console.log('received:', data)); + let global; + try { + global = await session.open(); + } catch (err) { + logger.error(`Error opening session to server ${options.host}: ${err}`); + process.exit(1); } - const global = await session.open(); - const engineVersion = await global.engineVersion(); - logger.verbose(`Created session to server ${options.host}, engine version is ${engineVersion.qComponentVersion}.`); + let engineVersion; + try { + engineVersion = await global.engineVersion(); + logger.verbose(`Server ${options.host} has engine version ${engineVersion.qComponentVersion}.`); + } catch (err) { + logger.error(`Error getting engine version from server ${options.host}: ${err}`); + process.exit(1); + } const app = await global.openDoc(options.appId, '', '', '', false); logger.verbose(`Opened app ${options.appId}.`); diff --git a/src/lib/cmd/getscript.js b/src/lib/cmd/getscript.js index 9317411..07f4ac5 100644 --- a/src/lib/cmd/getscript.js +++ b/src/lib/cmd/getscript.js @@ -1,5 +1,6 @@ const enigma = require('enigma.js'); -const { setupEnigmaConnection } = require('../util/enigma'); + +const { setupEnigmaConnection, addTrafficLogging } = require('../util/enigma'); const { logger, setLoggingLevel, isPkg, execPath } = require('../../globals'); /** @@ -17,20 +18,40 @@ const getScript = async (options) => { logger.verbose('Get app script'); logger.debug(`Options: ${JSON.stringify(options, null, 2)}`); - // Configure Enigma.js - const configEnigma = await setupEnigmaConnection(options); + // Session ID to use when connecting to the Qlik Sense server + const sessionId = 'ctrlq'; - const session = enigma.create(configEnigma); - if (options.logLevel === 'silly') { - // eslint-disable-next-line no-console - session.on('traffic:sent', (data) => console.log('sent:', data)); - // eslint-disable-next-line no-console - session.on('traffic:received', (data) => console.log('received:', data)); + // Create new session to Sense engine + let configEnigma; + let session; + try { + configEnigma = await setupEnigmaConnection(options, sessionId); + session = await enigma.create(configEnigma); + logger.verbose(`Created session to server ${options.host}.`); + } catch (err) { + logger.error(`Error creating session to server ${options.host}: ${err}`); + process.exit(1); } - const global = await session.open(); - const engineVersion = await global.engineVersion(); - logger.verbose(`Created session to server ${options.host}, engine version is ${engineVersion.qComponentVersion}.`); + // Set up logging of websocket traffic + addTrafficLogging(session, options); + + let global; + try { + global = await session.open(); + } catch (err) { + logger.error(`Error opening session to server ${options.host}: ${err}`); + process.exit(1); + } + + let engineVersion; + try { + engineVersion = await global.engineVersion(); + logger.verbose(`Server ${options.host} has engine version ${engineVersion.qComponentVersion}.`); + } catch (err) { + logger.error(`Error getting engine version from server ${options.host}: ${err}`); + process.exit(1); + } const app = await global.openDoc(options.appId, '', '', '', false); logger.verbose(`Opened app ${options.appId}.`); diff --git a/src/lib/cmd/getvariable.js b/src/lib/cmd/getvariable.js index 1afbdd5..7a8853f 100644 --- a/src/lib/cmd/getvariable.js +++ b/src/lib/cmd/getvariable.js @@ -3,7 +3,7 @@ const enigma = require('enigma.js'); const { table } = require('table'); -const { setupEnigmaConnection } = require('../util/enigma'); +const { setupEnigmaConnection, addTrafficLogging } = require('../util/enigma'); const { getApps } = require('../util/app'); const { logger, setLoggingLevel, isPkg, execPath } = require('../../globals'); @@ -52,23 +52,45 @@ const getVariable = async (options) => { // Get IDs of all apps that should be processed const apps = await getApps(options, options.appId, options.appTag); - // Configure Enigma.js - const configEnigma = await setupEnigmaConnection(options); + // Session ID to use when connecting to the Qlik Sense server + const sessionId = 'ctrlq'; + + // Create new session to Sense engine + let configEnigma; + let session; + try { + configEnigma = await setupEnigmaConnection(options, sessionId); + session = await enigma.create(configEnigma); + logger.verbose(`Created session to server ${options.host}.`); + } catch (err) { + logger.error(`Error creating session to server ${options.host}: ${err}`); + process.exit(1); + } + + // Set up logging of websocket traffic + addTrafficLogging(session, options); + + let global; + try { + global = await session.open(); + } catch (err) { + logger.error(`Error opening session to server ${options.host}: ${err}`); + process.exit(1); + } + + let engineVersion; + try { + engineVersion = await global.engineVersion(); + logger.verbose(`Server ${options.host} has engine version ${engineVersion.qComponentVersion}.`); + } catch (err) { + logger.error(`Error getting engine version from server ${options.host}: ${err}`); + process.exit(1); + } let allVariables = []; let subsetVariables = []; for (const app of apps) { - const session = enigma.create(configEnigma); - if (options.logLevel === 'silly') { - session.on('traffic:sent', (data) => console.log('sent:', data)); - session.on('traffic:received', (data) => console.log('received:', data)); - } - const global = await session.open(); - - const engineVersion = await global.engineVersion(); - logger.verbose(`Created session to server ${options.host}, engine version is ${engineVersion.qComponentVersion}.`); - // Open app without data const doc = await global.openDoc(app.id, '', '', '', true); logger.verbose(`Opened app ${app.id}, "${app.name}".`); @@ -104,7 +126,7 @@ const getVariable = async (options) => { allVariables = allVariables.concat({ appId: app.id, appName: app.name, variables: appVariablesLayout.qVariableList.qItems }); // Close app session - doc.session.close(); + // doc.session.close(); } if (options.variable === undefined) { @@ -218,6 +240,12 @@ const getVariable = async (options) => { } else { logger.error('Undefined --output-format option'); } + + if ((await session.close()) === true) { + logger.verbose(`Closed session after getting master item measures in app ${options.appId} on host ${options.host}`); + } else { + logger.error(`Error closing session for app ${options.appId} on host ${options.host}`); + } } catch (err) { logger.error(`GET VARIABLE: ${err.stack}`); } diff --git a/src/lib/cmd/import-masteritem-excel.js b/src/lib/cmd/import-masteritem-excel.js index dadec6f..c2c950f 100644 --- a/src/lib/cmd/import-masteritem-excel.js +++ b/src/lib/cmd/import-masteritem-excel.js @@ -4,7 +4,7 @@ const enigma = require('enigma.js'); const xlsx = require('node-xlsx').default; const uuidCreate = require('uuid').v4; -const { setupEnigmaConnection } = require('../util/enigma'); +const { setupEnigmaConnection, addTrafficLogging } = require('../util/enigma'); const { logger, setLoggingLevel, isPkg, execPath, verifyFileExists, sleep } = require('../../globals'); let importCount = 0; @@ -78,46 +78,6 @@ const createColorMap = async (app, colorMapId, newPerValueColorMap) => { return colorMapId; }; -// Function to add logging of session's websocket traffic -const addTrafficLogging = (session, options) => { - if (options.logLevel === 'silly') { - session.on('traffic:sent', (data) => console.log('sent:', data)); - - session.on('traffic:received', (data) => { - console.log('received:', data); - if (data?.result?.qReturn) { - console.log(`qReturn: ${JSON.stringify(data.result.qReturn, null, 2)}`); - } - - if (data?.result?.qInfo) { - console.log(`qInfo: ${JSON.stringify(data.result.qInfo, null, 2)}`); - } - - if (data?.change?.length > 1) { - console.log(`change length > 1: ${JSON.stringify(data.change, null, 2)}`); - - console.log('received:', data); - if (data?.result?.qReturn) { - console.log(`qReturn: ${JSON.stringify(data.result.qReturn, null, 2)}`); - } - - if (data?.result?.qInfo) { - console.log(`qInfo: ${JSON.stringify(data.result.qInfo, null, 2)}`); - } - } - }); - - session.on('notification:*', (eventName, data) => { - console.log(`SESSION EVENT=${eventName}: `, data); - }); - - session.on('closed', (code, message) => { - console.log(`SESSION CLOSED, code=${code}, message="${message}"`); - process.exit(1); - }); - } -}; - // Create master dimension using Enigma.js const createDimension = async (options, app, dimensionDefRow, colPos, newPerValueColorMap, newDimensionColor, importLimit) => { // Create a new master dimension in the app @@ -582,7 +542,6 @@ const updateMeasure = async (options, existingMeasure, app, measureDefRow, colPo delete measureData.qMeasure.coloring.gradient; } - logger.verbose(`Updating existing measure "${measureData.qMetaDef.title}"`); logger.debug(`Measure data: ${JSON.stringify(measureData, null, 2)}`); // Update existing measure with new data @@ -716,7 +675,7 @@ const validateMasterMeasureFields = (masterItemDefRow, colPos) => { // Take 10 items at a time from defintions array and creates master items for them. // Repeat until all definitions have been processed. -const createMasterItems = async (masterItemDefs, options, colPos, existingMeasures, existingDimensions) => { +const createMasterItems = async (masterItemDefs, options, colPos, existingMeasures, existingDimensions, session) => { const masterItemDefinitions = masterItemDefs.slice(); // Remove header row @@ -744,13 +703,29 @@ const createMasterItems = async (masterItemDefs, options, colPos, existingMeasur const masterItemBatch = masterItemDefinitions.splice(0, 10); // Create new session to Sense engine - const configEnigma = await setupEnigmaConnection(options); - const session = await enigma.create(configEnigma); - - // Set up logging of websocket traffic - addTrafficLogging(session, options); - - const global = await session.open(); + // let configEnigma; + // let session; + // try { + // console.log('A1'); + // configEnigma = await setupEnigmaConnection(options, sessionId); + // console.log('A2'); + // session = await enigma.create(configEnigma); + // console.log('A3'); + // } catch (err) { + // logger.error(`Error creating session to server ${options.host}: ${err}`); + // process.exit(1); + // } + + // // Set up logging of websocket traffic + // addTrafficLogging(session, options); + + let global; + try { + global = await session.open(); + } catch (err) { + logger.error(`Error opening session to server ${options.host}: ${err}`); + process.exit(1); + } const engineVersion = await global.engineVersion(); logger.verbose(`Created session to server ${options.host}, engine version is ${engineVersion.qComponentVersion}.`); @@ -963,11 +938,17 @@ const createMasterItems = async (masterItemDefs, options, colPos, existingMeasur } } - if ((await session.close()) === true) { - logger.verbose(`Closed session after adding/updating master items in app ${options.appId} on host ${options.host}`); - } else { - logger.error(`Error closing session for app ${options.appId} on host ${options.host}`); - } + // console.log('session.close 1'); + // if ((await session.close()) === true) { + // logger.verbose(`Closed session after adding/updating 10 master items in app ${options.appId} on host ${options.host}`); + + // // Wait 2 sec before creating a new session + // // This will help avoiding the 5 concurrent session limit + // logger.debug(`Sleeping for 2 s`); + // await sleep(2000); + // } else { + // logger.error(`Error closing session for app ${options.appId} on host ${options.host}`); + // } } }; @@ -1030,22 +1011,45 @@ const importMasterItemFromExcel = async (options) => { // --col-master-item-color const colPosMasterItemPerValueColor = getColumnPos(options, options.colMasterItemPerValueColor, sheet.data[0]); - // Configure Enigma.js - const configEnigma = await setupEnigmaConnection(options); + // Session ID to use when connecting to the Qlik Sense server + const sessionId = 'ctrlq'; - const session = enigma.create(configEnigma); + // Create new session to Sense engine + let configEnigma; + let session; + try { + configEnigma = await setupEnigmaConnection(options, sessionId); + session = await enigma.create(configEnigma); + logger.verbose(`Created session to server ${options.host}.`); + } catch (err) { + logger.error(`Error creating session to server ${options.host}: ${err}`); + process.exit(1); + } // Set up logging of websocket traffic addTrafficLogging(session, options); - const global = await session.open(); + let global; + try { + global = await session.open(); + } catch (err) { + logger.error(`Error opening session to server ${options.host}: ${err}`); + process.exit(1); + } - const engineVersion = await global.engineVersion(); - logger.verbose(`Created session to server ${options.host}, engine version is ${engineVersion.qComponentVersion}.`); + let engineVersion; + try { + engineVersion = await global.engineVersion(); + logger.verbose(`Server ${options.host} has engine version ${engineVersion.qComponentVersion}.`); + } catch (err) { + logger.error(`Error getting engine version from server ${options.host}: ${err}`); + process.exit(1); + } + // console.log('B6'); const app = await global.openDoc(options.appId, '', '', '', false); logger.verbose(`Opened app ${options.appId}.`); - + // console.log('B7'); // Get list of all existing master dimensions and measures // https://help.qlik.com/en-US/sense-developer/May2021/APIs/EngineAPI/definitions-NxLibraryDimensionDef.html const dimensionCall = { @@ -1088,13 +1092,19 @@ const importMasterItemFromExcel = async (options) => { const measuresLayout = await measuresModel.getLayout(); // Close session - if ((await session.close()) === true) { - logger.verbose( - `Closed session after reading list of existing master measure & dimensions in app ${options.appId} on host ${options.host}` - ); - } else { - logger.error(`Error closing session for app ${options.appId} on host ${options.host}`); - } + // console.log('session.close 2'); + // if ((await session.close()) === true) { + // logger.verbose( + // `Closed session after reading list of existing master measure & dimensions in app ${options.appId} on host ${options.host}` + // ); + + // // Wait 2 sec before continuing + // // This will help avoiding the 5 concurrent session limit + // logger.debug(`Sleeping for 2 s`); + // await sleep(2000); + // } else { + // logger.error(`Error closing session for app ${options.appId} on host ${options.host}`); + // } // Loop through rows in Excel file, extracting data for rows flagged as master items // let importCount = 0; @@ -1113,28 +1123,29 @@ const importMasterItemFromExcel = async (options) => { colPosMasterItemPerValueColor, }, measuresLayout.qMeasureList.qItems, - dimsLayout.qDimensionList.qItems + dimsLayout.qDimensionList.qItems, + session ); } logger.info(`Imported ${importCount} master items from Excel file ${options.file}`); // const resSave = await app.doSave(); - - // if ((await session.close()) === true) { - // logger.verbose(`Closed session after adding/updating master items in app ${options.appId} on host ${options.host}`); - // } else { - // logger.error(`Error closing session for app ${options.appId} on host ${options.host}`); - // } + // console.log('session.close 1'); + if ((await session.close()) === true) { + logger.verbose(`Closed session after adding/updating master items in app ${options.appId} on host ${options.host}`); + } else { + logger.error(`Error closing session for app ${options.appId} on host ${options.host}`); + } } catch (err) { logger.error(err.stack); } }; -const importMasterItemFromFile = (options) => { +const importMasterItemFromFile = async (options) => { if (options.fileType === 'excel') { // Source file type is Excel - importMasterItemFromExcel(options); + await importMasterItemFromExcel(options); } }; diff --git a/src/lib/cmd/importapp.js b/src/lib/cmd/importapp.js index 3967040..f2093de 100644 --- a/src/lib/cmd/importapp.js +++ b/src/lib/cmd/importapp.js @@ -78,7 +78,9 @@ const importAppFromFile = async (options) => { // Import apps specified in Excel file const importedApps = await qlikSenseApps.importAppsFromFiles(appsFromFile, tagsExisting, cpExisting); logger.debug(`Imported apps:\n${JSON.stringify(importedApps, null, 2)}`); + return importedApps; } + return false; } catch (err) { logger.error(`IMPORT APP: ${err.stack}`); } diff --git a/src/lib/cmd/scramblefield.js b/src/lib/cmd/scramblefield.js index c4ad59b..df0991d 100644 --- a/src/lib/cmd/scramblefield.js +++ b/src/lib/cmd/scramblefield.js @@ -1,6 +1,6 @@ const enigma = require('enigma.js'); -const { setupEnigmaConnection } = require('../util/enigma'); +const { setupEnigmaConnection, addTrafficLogging } = require('../util/enigma'); const { logger, setLoggingLevel, isPkg, execPath } = require('../../globals'); /** @@ -18,20 +18,40 @@ const scrambleField = async (options) => { logger.info('Scramble field'); logger.debug(`Options: ${JSON.stringify(options, null, 2)}`); - // Configure Enigma.js - const configEnigma = await setupEnigmaConnection(options); + // Session ID to use when connecting to the Qlik Sense server + const sessionId = 'ctrlq'; - const session = enigma.create(configEnigma); - if (options.logLevel === 'silly') { - // eslint-disable-next-line no-console - session.on('traffic:sent', (data) => console.log('sent:', data)); - // eslint-disable-next-line no-console - session.on('traffic:received', (data) => console.log('received:', data)); + // Create new session to Sense engine + let configEnigma; + let session; + try { + configEnigma = await setupEnigmaConnection(options, sessionId); + session = await enigma.create(configEnigma); + logger.verbose(`Created session to server ${options.host}.`); + } catch (err) { + logger.error(`Error creating session to server ${options.host}: ${err}`); + process.exit(1); } - const global = await session.open(); - const engineVersion = await global.engineVersion(); - logger.verbose(`Created session to server ${options.host}, engine version is ${engineVersion.qComponentVersion}.`); + // Set up logging of websocket traffic + addTrafficLogging(session, options); + + let global; + try { + global = await session.open(); + } catch (err) { + logger.error(`Error opening session to server ${options.host}: ${err}`); + process.exit(1); + } + + let engineVersion; + try { + engineVersion = await global.engineVersion(); + logger.verbose(`Server ${options.host} has engine version ${engineVersion.qComponentVersion}.`); + } catch (err) { + logger.error(`Error getting engine version from server ${options.host}: ${err}`); + process.exit(1); + } const app = await global.openDoc(options.appId, '', '', '', false); logger.verbose(`Opened app ${options.appId}.`); diff --git a/src/lib/cmd/testconnection.js b/src/lib/cmd/testconnection.js index 0ac9fe1..9b7a719 100644 --- a/src/lib/cmd/testconnection.js +++ b/src/lib/cmd/testconnection.js @@ -22,7 +22,8 @@ const testConnection = async (options) => { logger.info(`Successfully connected to Qlik Sense server ${options.host} on port ${options.port}`); logger.info(`Qlik Sense repository build version: ${aboutInfo.buildVersion}`); logger.info(`Qlik Sense repository build date: ${aboutInfo.buildDate}`); - return true; + + return aboutInfo; } catch (err) { logger.error(`EXPORT APP: ${err.stack}`); return false; diff --git a/src/lib/util/about.js b/src/lib/util/about.js index 950bdfb..9a70d01 100644 --- a/src/lib/util/about.js +++ b/src/lib/util/about.js @@ -8,20 +8,25 @@ function getAboutFromQseow(options) { return new Promise((resolve, reject) => { logger.verbose(`Getting about info from QSEoW...`); - // Make sure certificates exist - const fileCert = path.resolve(execPath, options.authCertFile); - const fileCertKey = path.resolve(execPath, options.authCertKeyFile); + // Should cerrificates be used for authentication? + let axiosConfig; + if (options.authType === 'cert') { + // Make sure certificates exist + const fileCert = path.resolve(execPath, options.authCertFile); + const fileCertKey = path.resolve(execPath, options.authCertKeyFile); - const axiosConfig = setupQRSConnection(options, { - method: 'get', - fileCert, - fileCertKey, - path: '/qrs/about', - // headers: { - // 'Accept': 'application/json', - // 'Content-Type': 'application/json; charset=utf-8', - // }, - }); + axiosConfig = setupQRSConnection(options, { + method: 'get', + fileCert, + fileCertKey, + path: '/qrs/about', + }); + } else if (options.authType === 'jwt') { + axiosConfig = setupQRSConnection(options, { + method: 'get', + path: '/qrs/about', + }); + } logger.debug(`About to get about info from QSEoW`); diff --git a/src/lib/util/app.js b/src/lib/util/app.js index 27d7bf3..857e630 100644 --- a/src/lib/util/app.js +++ b/src/lib/util/app.js @@ -58,16 +58,18 @@ async function getApps(options, idArray, tagArray) { } logger.debug(`GET APPS: QRS query filter (incl ids, tags): ${filter}`); - // Make sure certificates exist - const fileCert = path.resolve(execPath, options.authCertFile); - const fileCertKey = path.resolve(execPath, options.authCertKeyFile); - let axiosConfig; if (filter === '') { // No apps matching the provided app IDs and tags. Error! logger.error('GET APPS: No apps matching the provided app IDs and and tags. Exiting.'); process.exit(1); - } else { + } + // Should cerrificates be used for authentication? + else if (options.authType === 'cert') { + // Make sure certificates exist + const fileCert = path.resolve(execPath, options.authCertFile); + const fileCertKey = path.resolve(execPath, options.authCertKeyFile); + axiosConfig = setupQRSConnection(options, { method: 'get', fileCert, @@ -75,6 +77,12 @@ async function getApps(options, idArray, tagArray) { path: '/qrs/app/full', queryParameters: [{ name: 'filter', value: filter }], }); + } else if (options.authType === 'jwt') { + axiosConfig = setupQRSConnection(options, { + method: 'get', + path: '/qrs/app/full', + queryParameters: [{ name: 'filter', value: filter }], + }); } const result = await axios.request(axiosConfig); @@ -110,16 +118,25 @@ async function getAppById(appId, optionsParam) { return false; } - // Make sure certificates exist - const fileCert = path.resolve(execPath, options.authCertFile); - const fileCertKey = path.resolve(execPath, options.authCertKeyFile); + // Should cerrificates be used for authentication? + let axiosConfig; + if (options.authType === 'cert') { + // Make sure certificates exist + const fileCert = path.resolve(execPath, options.authCertFile); + const fileCertKey = path.resolve(execPath, options.authCertKeyFile); - const axiosConfig = setupQRSConnection(options, { - method: 'get', - fileCert, - fileCertKey, - path: `/qrs/app/${appId}`, - }); + axiosConfig = setupQRSConnection(options, { + method: 'get', + fileCert, + fileCertKey, + path: `/qrs/app/${appId}`, + }); + } else if (options.authType === 'jwt') { + axiosConfig = setupQRSConnection(options, { + method: 'get', + path: `/qrs/app/${appId}`, + }); + } const result = await axios.request(axiosConfig); logger.debug(`GET APP BY ID: Result=${result.status}`); @@ -129,11 +146,11 @@ async function getAppById(appId, optionsParam) { logger.debug(`GET APP BY ID: App details: ${app}`); if (app && app?.id) { - // Yes, the task exists + // Yes, the app exists logger.verbose(`App exists: ID=${app.id}. App name="${app.name}"`); return app; } - // No, the task does not exist + // No, the app does not exist logger.verbose(`App does not exist: ID=${appId}`); } @@ -151,23 +168,38 @@ async function getAppById(appId, optionsParam) { } // Function to delete app given app ID -async function deleteAppById(appId) { +async function deleteAppById(appId, options) { + // Ensuire options are specified. Exit if not + if (!options) { + logger.error(`DELETE APP: No options specified. Exiting.`); + process.exit(1); + } + try { logger.debug(`DELETE APP: Starting delete app from QSEoW for app id ${appId}`); // Get CLI options - const cliOptions = getCliOptions(); + // const cliOptions = getCliOptions(); - // Make sure certificates exist - const fileCert = path.resolve(execPath, cliOptions.authCertFile); - const fileCertKey = path.resolve(execPath, cliOptions.authCertKeyFile); + // Should cerrificates be used for authentication? + let axiosConfig; + if (options.authType === 'cert') { + // Make sure certificates exist + const fileCert = path.resolve(execPath, options.authCertFile); + const fileCertKey = path.resolve(execPath, options.authCertKeyFile); - const axiosConfig = setupQRSConnection(cliOptions, { - method: 'delete', - fileCert, - fileCertKey, - path: `/qrs/app/${appId}`, - }); + axiosConfig = setupQRSConnection(options, { + method: 'delete', + fileCert, + fileCertKey, + path: `/qrs/app/${appId}`, + }); + } else if (options.authType === 'jwt') { + axiosConfig = setupQRSConnection(options, { + method: 'delete', + path: `/qrs/app/${appId}`, + }); + } const result = await axios.request(axiosConfig); logger.debug(`DELETE APP: Result=result.status`); @@ -190,8 +222,78 @@ async function deleteAppById(appId) { } } +// Check if an app with a given id exists +async function appExistById(appId, options) { + try { + logger.debug(`Checking if app with id ${appId} exists in QSEoW`); + + // Is the app ID a valid GUID? + if (!validate(appId)) { + logger.error(`APP EXIST BY ID: App ID ${appId} is not a valid GUID.`); + + return false; + } + + // Should cerrificates be used for authentication? + let axiosConfig; + if (options.authType === 'cert') { + // Make sure certificates exist + const fileCert = path.resolve(execPath, options.authCertFile); + const fileCertKey = path.resolve(execPath, options.authCertKeyFile); + + axiosConfig = setupQRSConnection(options, { + method: 'get', + fileCert, + fileCertKey, + path: '/qrs/app', + queryParameters: [{ name: 'filter', value: encodeURI(`id eq ${appId}`) }], + }); + } else if (options.authType === 'jwt') { + axiosConfig = setupQRSConnection(options, { + method: 'get', + path: '/qrs/app', + queryParameters: [{ name: 'filter', value: encodeURI(`id eq ${appId}`) }], + }); + } + + const result = await axios.request(axiosConfig); + logger.debug(`APP EXIST BY ID: Result=${result.status}`); + + if (result.status === 200) { + const apps = JSON.parse(result.data); + logger.debug(`APP EXIST BY ID: App details: ${JSON.stringify(apps)}`); + + if (apps.length === 1 && apps[0].id) { + // Yes, the app exists + logger.verbose(`App exists: ID=${apps[0].id}. App name="${apps[0].name}"`); + + return true; + } + + if (apps.length > 1) { + logger.error(`More than one app with ID ${appId} found. Should not be possible. Exiting.`); + process.exit(1); + } else { + return false; + } + } + + return false; + } catch (err) { + logger.error(`APP EXIST BY ID: ${err}`); + + // Show stack trace if available + if (err?.stack) { + logger.error(`APP EXIST BY ID:\n ${err.stack}`); + } + + return false; + } +} + module.exports = { getApps, getAppById, deleteAppById, + appExistById, }; diff --git a/src/lib/util/assert-options.js b/src/lib/util/assert-options.js index b530919..4efea24 100644 --- a/src/lib/util/assert-options.js +++ b/src/lib/util/assert-options.js @@ -18,24 +18,33 @@ const sharedParamAssertOptions = async (options) => { logger.debug(`authCertFile: ${options.authCertFile}`); logger.debug(`authCertKeyFile: ${options.authCertKeyFile}`); - // Verify that certificate files exists (if specified) - const fileCert = path.resolve(execPath, options.authCertFile); - const fileCertKey = path.resolve(execPath, options.authCertKeyFile); - - const fileCertExists = await verifyFileExists(fileCert); - if (fileCertExists === false) { - logger.error(`Missing certificate file ${fileCert}. Aborting`); - process.exit(1); - } else { - logger.verbose(`Certificate file ${fileCert} found`); - } + // If certificate authentication is used: certs and user dir/id must be present. + if (options.authType === 'cert') { + // Verify that certificate files exists (if specified) + const fileCert = path.resolve(execPath, options.authCertFile); + const fileCertKey = path.resolve(execPath, options.authCertKeyFile); + + const fileCertExists = await verifyFileExists(fileCert); + if (fileCertExists === false) { + logger.error(`Missing certificate file ${fileCert}. Aborting`); + process.exit(1); + } else { + logger.verbose(`Certificate file ${fileCert} found`); + } - const fileCertKeyExists = await verifyFileExists(fileCertKey); - if (fileCertKeyExists === false) { - logger.error(`Missing certificate key file ${fileCertKey}. Aborting`); - process.exit(1); - } else { - logger.verbose(`Certificate key file ${fileCertKey} found`); + const fileCertKeyExists = await verifyFileExists(fileCertKey); + if (fileCertKeyExists === false) { + logger.error(`Missing certificate key file ${fileCertKey}. Aborting`); + process.exit(1); + } else { + logger.verbose(`Certificate key file ${fileCertKey} found`); + } + } else if (options.authType === 'jwt') { + // Verify that --auth-jwt parameter is specified + if (options.authJwt === undefined || !options.authJwt) { + logger.error('Mandatory option --auth-jwt is missing. Use it to specify the JWT token to use for authentication.'); + process.exit(1); + } } }; diff --git a/src/lib/util/customproperties.js b/src/lib/util/customproperties.js index 5b13954..5043063 100644 --- a/src/lib/util/customproperties.js +++ b/src/lib/util/customproperties.js @@ -8,16 +8,25 @@ function getCustomPropertiesFromQseow(options) { return new Promise((resolve, reject) => { logger.verbose(`Getting custom properties from QSEoW...`); - // Make sure certificates exist - const fileCert = path.resolve(execPath, options.authCertFile); - const fileCertKey = path.resolve(execPath, options.authCertKeyFile); - - const axiosConfig = setupQRSConnection(options, { - method: 'get', - fileCert, - fileCertKey, - path: '/qrs/custompropertydefinition/full', - }); + // Should cerrificates be used for authentication? + let axiosConfig; + if (options.authType === 'cert') { + // Make sure certificates exist + const fileCert = path.resolve(execPath, options.authCertFile); + const fileCertKey = path.resolve(execPath, options.authCertKeyFile); + + axiosConfig = setupQRSConnection(options, { + method: 'get', + fileCert, + fileCertKey, + path: '/qrs/custompropertydefinition/full', + }); + } else if (options.authType === 'jwt') { + axiosConfig = setupQRSConnection(options, { + method: 'get', + path: '/qrs/custompropertydefinition/full', + }); + } axios .request(axiosConfig) diff --git a/src/lib/util/enigma.js b/src/lib/util/enigma.js index bd72de6..60b62c3 100644 --- a/src/lib/util/enigma.js +++ b/src/lib/util/enigma.js @@ -4,39 +4,155 @@ const path = require('path'); const { logger, execPath, readCert } = require('../../globals'); -const setupEnigmaConnection = async (options) => { +const setupEnigmaConnection = async (options, sessionId) => { logger.debug('Prepping for Enigma connection...'); - logger.verbose('Verify that cert files exists'); - - const fileCert = path.resolve(execPath, options.authCertFile); - const fileCertKey = path.resolve(execPath, options.authCertKeyFile); - // eslint-disable-next-line global-require, import/no-dynamic-require const qixSchema = require(`enigma.js/schemas/${options.schemaVersion}`); - return { - schema: qixSchema, - url: SenseUtilities.buildUrl({ - host: options.host, - port: options.enginePort !== undefined ? options.enginePort : options.port, - prefix: options.virtualProxy, - secure: options.secure, - appId: options.appId, - }), - createSocket: (url) => - new WebSocket(url, { - key: readCert(fileCertKey), - cert: readCert(fileCert), - headers: { - 'X-Qlik-User': `UserDirectory=${options.authUserDir};UserId=${options.authUserId}`, - }, - rejectUnauthorized: false, + let enigmaConfig; + // Should certificates be used for authentication? + if (options.authType === 'cert') { + logger.verbose(`Using certificates for authentication with Enigma`); + + logger.verbose('Verify that cert files exists'); + const fileCert = path.resolve(execPath, options.authCertFile); + const fileCertKey = path.resolve(execPath, options.authCertKeyFile); + + if (!fileCert || !fileCertKey) { + logger.error(`Certificate file(s) not found when setting up Enigma connection`); + process.exit(1); + } + + // Set up Enigma configuration + // buildUrl docs: https://github.com/qlik-oss/enigma.js/blob/master/docs/api.md#senseutilitiesbuildurlconfig + enigmaConfig = { + schema: qixSchema, + url: SenseUtilities.buildUrl({ + host: options.host, + port: options.enginePort !== undefined ? options.enginePort : options.port, + prefix: options.virtualProxy, + route: 'app/engineData', + secure: options.secure, + appId: options.appId, }), - protocol: { delta: true }, - }; + createSocket: (url) => + new WebSocket(url, { + key: readCert(fileCertKey), + cert: readCert(fileCert), + headers: { + 'X-Qlik-User': `UserDirectory=${options.authUserDir};UserId=${options.authUserId}`, + }, + rejectUnauthorized: false, + }), + protocol: { delta: true }, + }; + } else if (options.authType === 'jwt') { + logger.verbose(`Using JWT for authentication with Enigma`); + + try { + logger.verbose('Building Enigma config using sessionId: ', sessionId); + + // Set up Enigma configuration + // buildUrl docs: https://github.com/qlik-oss/enigma.js/blob/master/docs/api.md#senseutilitiesbuildurlconfig + enigmaConfig = { + schema: qixSchema, + url: SenseUtilities.buildUrl({ + host: options.host, + port: options.enginePort !== undefined ? options.enginePort : options.port, + prefix: options.virtualProxy, + route: 'app/engineData', + secure: options.secure, + appId: options.appId, + ttl: 5, + identity: sessionId || undefined, + }), + createSocket: (url) => + new WebSocket(url, { + headers: { + Authorization: `Bearer ${options.authJwt}`, + }, + rejectUnauthorized: false, + }), + protocol: { delta: true }, + }; + } catch (err) { + logger.error(`Error when setting up Enigma connection: ${err}`); + process.exit(1); + } + } + + return enigmaConfig; +}; + +// Function to add logging of session's websocket traffic +const addTrafficLogging = (session, options) => { + session.on('notification:*', (eventName, data) => { + // console.log(`SESSION EVENT=${eventName}: `, data); + + if (eventName === 'EVENT=OnAuthenticationInformation') { + // Authentication successful + logger.verbose(`Session event "${eventName}": ${JSON.stringify(data, null, 2)}`); + } else if (eventName === 'OnConnected') { + // Session created + logger.verbose(`Session event "${eventName}": ${JSON.stringify(data, null, 2)}`); + } else if (eventName === 'OnMaxParallelSessionsExceeded') { + // Too many concurrent sessions + logger.error(`Session event "${eventName}": ${JSON.stringify(data, null, 2)}`); + } else { + logger.verbose(`Session event "${eventName}": ${JSON.stringify(data, null, 2)}`); + } + }); + + session.on('closed', (code, message) => { + logger.verbose(`Session closed`); + // logger.verbose (`Session closed, code=${code}, message="${message}"`); + // console.log(JSON.stringify(code, null, 2)); + }); + + session.on('opened', (code, message) => { + logger.verbose(`SESSION opened, code=${code}, message="${message}"`); + }); + + if (options.logLevel === 'silly') { + session.on('traffic:sent', (data) => console.log('sent:', data)); + + session.on('traffic:received', (data) => { + console.log('received:', data); + if (data?.result?.qReturn) { + console.log(`qReturn: ${JSON.stringify(data.result.qReturn, null, 2)}`); + } + + if (data?.result?.qInfo) { + console.log(`qInfo: ${JSON.stringify(data.result.qInfo, null, 2)}`); + } + + if (data?.change?.length > 1) { + console.log(`change length > 1: ${JSON.stringify(data.change, null, 2)}`); + + console.log('received:', data); + if (data?.result?.qReturn) { + console.log(`qReturn: ${JSON.stringify(data.result.qReturn, null, 2)}`); + } + + if (data?.result?.qInfo) { + console.log(`qInfo: ${JSON.stringify(data.result.qInfo, null, 2)}`); + } + } + }); + + session.on('notification:*', (eventName, data) => { + console.log(`SESSION EVENT=${eventName}: `, data); + }); + + session.on('closed', (code, message) => { + console.log(`SESSION CLOSED, code=${code}, message="${message}"`); + process.exit(1); + }); + } }; module.exports = { setupEnigmaConnection, + addTrafficLogging, }; diff --git a/src/lib/util/qrs.js b/src/lib/util/qrs.js index 1ce9afa..2638a33 100644 --- a/src/lib/util/qrs.js +++ b/src/lib/util/qrs.js @@ -16,32 +16,76 @@ const setupQRSConnection = (options, param) => { process.exit(1); } - const httpsAgent = new https.Agent({ - rejectUnauthorized: false, - cert: readCert(param.fileCert), - key: readCert(param.fileCertKey), - }); - // Port is specified slightly differently for different Ctrl-Q commands const port = options.qrsPort === undefined ? options.port : options.qrsPort; // Set up Sense repository service configuration const xrfKey = generateXrfKey(); - const axiosConfig = { - url: `${param.path}?xrfkey=${xrfKey}`, - method: param.method.toLowerCase(), - baseURL: `https://${options.host}:${port}`, - headers: { - 'x-qlik-xrfkey': xrfKey, - 'X-Qlik-User': `UserDirectory=${options.authUserDir};UserId=${options.authUserId}`, - }, - responseType: 'application/json', - responseEncoding: 'utf8', - httpsAgent, - timeout: 60000, - // passphrase: "YYY" - }; + // Sanitize virtual proxy prefix + // If the virtual proxy proxydoes not start with a slash, add it + // If the virtual proxy ends with a slash, remove it + let newVirtualProxy = options.virtualProxy; + if (options.virtualProxy) { + newVirtualProxy = options.virtualProxy.replace(/\/$/, ''); + + // If the virtual proxy length is longer than 1 and does not start with a slash, add it + if (newVirtualProxy.length > 1 && !newVirtualProxy.startsWith('/')) { + newVirtualProxy = `/${newVirtualProxy}`; + } + } + + // If param.path starts with a slash, remove it + let newPath = param.path; + if (param.path) { + newPath = param.path.replace(/^\//, ''); + } + + let axiosConfig; + // Should cerrificates be used for authentication? + if (options.authType === 'cert') { + logger.verbose(`Using certificates for authentication with QRS`); + + const httpsAgent = new https.Agent({ + rejectUnauthorized: false, + cert: readCert(param.fileCert), + key: readCert(param.fileCertKey), + }); + + axiosConfig = { + url: `${newVirtualProxy}/${newPath}?xrfkey=${xrfKey}`, + method: param.method.toLowerCase(), + baseURL: `https://${options.host}:${port}`, + headers: { + 'x-qlik-xrfkey': xrfKey, + 'X-Qlik-User': `UserDirectory=${options.authUserDir};UserId=${options.authUserId}`, + }, + responseType: 'application/json', + responseEncoding: 'utf8', + httpsAgent, + timeout: 60000, + }; + } else if (options.authType === 'jwt') { + logger.verbose(`Using JWT for authentication with QRS`); + + const httpsAgent = new https.Agent({ + rejectUnauthorized: false, + }); + + axiosConfig = { + url: `${newVirtualProxy}/${newPath}?xrfkey=${xrfKey}`, + method: param.method.toLowerCase(), + baseURL: `https://${options.host}:${port}`, + headers: { + 'x-qlik-xrfkey': xrfKey, + Authorization: `Bearer ${options.authJwt}`, + }, + responseType: 'application/json', + responseEncoding: 'utf8', + httpsAgent, + timeout: 60000, + }; + } // Add message body (if any) if (param.body) { diff --git a/src/lib/util/tag.js b/src/lib/util/tag.js index 6d0602b..745ef2a 100644 --- a/src/lib/util/tag.js +++ b/src/lib/util/tag.js @@ -8,20 +8,25 @@ function getTagsFromQseow(options) { return new Promise((resolve, reject) => { logger.verbose(`Getting tags from QSEoW...`); - // Make sure certificates exist - const fileCert = path.resolve(execPath, options.authCertFile); - const fileCertKey = path.resolve(execPath, options.authCertKeyFile); + // Should cerrificates be used for authentication? + let axiosConfig; + if (options.authType === 'cert') { + // Make sure certificates exist + const fileCert = path.resolve(execPath, options.authCertFile); + const fileCertKey = path.resolve(execPath, options.authCertKeyFile); - const axiosConfig = setupQRSConnection(options, { - method: 'get', - fileCert, - fileCertKey, - path: '/qrs/tag/full', - // headers: { - // 'Accept': 'application/json', - // 'Content-Type': 'application/json; charset=utf-8', - // }, - }); + axiosConfig = setupQRSConnection(options, { + method: 'get', + fileCert, + fileCertKey, + path: '/qrs/tag/full', + }); + } else if (options.authType === 'jwt') { + axiosConfig = setupQRSConnection(options, { + method: 'get', + path: '/qrs/tag/full', + }); + } logger.debug(`About to retrieve tags from QRS API.`); diff --git a/src/lib/util/task.js b/src/lib/util/task.js index c4602e3..6c1077b 100644 --- a/src/lib/util/task.js +++ b/src/lib/util/task.js @@ -28,18 +28,28 @@ async function taskExistById(taskId, optionsParam) { return false; } - // Make sure certificates exist - const fileCert = path.resolve(execPath, options.authCertFile); - const fileCertKey = path.resolve(execPath, options.authCertKeyFile); - - // const filter = encodeURI(`name eq '👍😎 updateSheetThumbnail'`); - const axiosConfig = setupQRSConnection(options, { - method: 'get', - fileCert, - fileCertKey, - path: '/qrs/task', - queryParameters: [{ name: 'filter', value: encodeURI(`id eq ${taskId}`) }], - }); + // Should cerrificates be used for authentication? + let axiosConfig; + if (options.authType === 'cert') { + // Make sure certificates exist + const fileCert = path.resolve(execPath, options.authCertFile); + const fileCertKey = path.resolve(execPath, options.authCertKeyFile); + + // const filter = encodeURI(`name eq '👍😎 updateSheetThumbnail'`); + axiosConfig = setupQRSConnection(options, { + method: 'get', + fileCert, + fileCertKey, + path: '/qrs/task', + queryParameters: [{ name: 'filter', value: encodeURI(`id eq ${taskId}`) }], + }); + } else if (options.authType === 'jwt') { + axiosConfig = setupQRSConnection(options, { + method: 'get', + path: '/qrs/task', + queryParameters: [{ name: 'filter', value: encodeURI(`id eq ${taskId}`) }], + }); + } const result = await axios.request(axiosConfig); logger.debug(`TASK EXIST BY ID: Result=${result.status}`); diff --git a/testdata/tasks.xlsx b/testdata/tasks.xlsx index ef0c850..b67c384 100644 Binary files a/testdata/tasks.xlsx and b/testdata/tasks.xlsx differ