From 8cbfe640132627a197b8cc79b50e138c4fab6545 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=B6ran=20Sander?= Date: Tue, 26 Nov 2024 08:05:29 +0100 Subject: [PATCH] feat(qseow): Add publish and replace options to field scramble command Implements #522 --- src/lib/cli/qseow-scramble-field.js | 32 ++- src/lib/cmd/qseow/scramblefield.js | 278 ++++++++++++++++++++------- src/lib/util/qseow/app.js | 36 ++++ src/lib/util/qseow/assert-options.js | 95 +++------ src/lib/util/qseow/stream.js | 45 ++++- 5 files changed, 332 insertions(+), 154 deletions(-) diff --git a/src/lib/cli/qseow-scramble-field.js b/src/lib/cli/qseow-scramble-field.js index 55de4cf..4f64107 100644 --- a/src/lib/cli/qseow-scramble-field.js +++ b/src/lib/cli/qseow-scramble-field.js @@ -21,7 +21,6 @@ export function setupQseowScrambleFieldCommand(qseow) { .option('--engine-port ', 'Qlik Sense server engine port (usually 4747 for cert auth, 443 for jwt auth)', '4747') .option('--qrs-port ', 'Qlik Sense server QRS port (usually 4242 for cert auth, 443 for jwt auth)', '4242') .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', '') .requiredOption( '--secure ', @@ -37,31 +36,30 @@ export function setupQseowScrambleFieldCommand(qseow) { .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('--app-id ', 'Qlik Sense app ID to be scrambled') .requiredOption('--field-name ', 'name of field(s) to be scrambled') - .requiredOption('--new-app-name ', 'name of new app that will contain scrambled data') + .requiredOption('--new-app-name ', 'name of new app that will contain scrambled data. Not used if --new-app-cmd=replace') - .addOption(new Option('--new-app-publish', 'publish scrambled app to a stream')) - .addOption(new Option('--new-app-publish-stream-id ', 'stream ID to publish scrambled app to').default('')) - .addOption(new Option('--new-app-publish-stream-name ', 'stream name to publish scrambled app to').default('')) - - .addOption(new Option('--new-app-publish-replace', 'publish-replace an existing, published app')) .addOption( new Option( - '--new-app-publish-replace-app-id ', - 'ID of published app that should be replaced by the new scrambled app' - ).default('') + '--new-app-cmd ', + 'what to do with the new app. If nothing is specified in this option the new app will be placed in My Work. WHen specifying "replace": If the replaced app is published, only the sheets that were originally published with the app are replaced. If the replaced app is not published, the entire app is replaced.' + ) + .choices(['publish', 'replace']) + .default('') ) + .addOption( - new Option( - '--new-app-publish-replace-app-name ', - 'Name of published app that should be replaced by the new scrambled app' - ).default('') + new Option('--new-app-cmd-id ', 'stream/app ID that --new-app-cmd acts on. Cannot be used with --new-app-cmd-name').default( + '' + ) ) - .addOption( - new Option('--new-app-delete-existing-unpublished', 'delete any already existing apps with same name as new scrambled app') + new Option( + '--new-app-cmd-name ', + 'stream/app name that --new-app-cmd acts on. Cannot be used with --new-app-cmd-id' + ).default('') ) - .addOption(new Option('--new-app-delete', 'delete the new scrambled app after all other operations are done')) .addOption(new Option('--force', 'force delete and replace operations to proceed without asking for confirmation')); } diff --git a/src/lib/cmd/qseow/scramblefield.js b/src/lib/cmd/qseow/scramblefield.js index 0eae3b2..9e060a0 100644 --- a/src/lib/cmd/qseow/scramblefield.js +++ b/src/lib/cmd/qseow/scramblefield.js @@ -1,10 +1,12 @@ import enigma from 'enigma.js'; import yesno from 'yesno'; +import { validate as uuidValidate } from 'uuid'; import { setupEnigmaConnection, addTrafficLogging } from '../../util/qseow/enigma_util.js'; import { logger, setLoggingLevel, isSea, execPath } from '../../../globals.js'; import { catchLog } from '../../util/log.js'; -import { deleteAppById, publishApp } from '../../util/qseow/app.js'; +import { deleteAppById, publishApp, replaceApp, getAppByName, getAppById } from '../../util/qseow/app.js'; +import { getStreamByName, getStreamById } from '../../util/qseow/stream.js'; /** * @@ -20,6 +22,108 @@ export async function scrambleField(options) { logger.debug(`Options: ${JSON.stringify(options, null, 2)}`); + // Keep track of the result of the scramble operation + const scrambleResult = { + newAppCmd: options.newAppCmd, + status: 'error', + }; + + // ------------------------------------------------ + // Verify parameters + + // --new-app-name is always required + if (!options.newAppName) { + logger.error('Option --new-app-name is required when --new-app-cmd is empty or set to "publish".'); + return scrambleResult; + } + + // No source app ID specified + if (!options.appId) { + logger.error('No source app ID specified.'); + return scrambleResult; + } + + // No fields specified + if (!options.fieldName || !Array.isArray(options.fieldName) || options?.fieldName?.length === 0) { + logger.error('No fields specified.'); + return scrambleResult; + } + + // Verify that --app-id is a valid GUID + if (!uuidValidate(options.appId)) { + logger.error(`Invalid GUID in --app-id: ${options.appId}`); + return scrambleResult; + } + + // Verify that source app exists, given --app-id + const appArray = await getAppById(options.appId, options); + if (appArray === false) { + logger.error(`App with ID ${options.appId} not found.`); + return scrambleResult; + } + + // Verify that --new-app-cmd is either '', 'publish' or 'replace' + if (options.newAppCmd !== '' && options.newAppCmd !== 'publish' && options.newAppCmd !== 'replace') { + logger.error(`Invalid value in --new-app-cmd: ${options.newAppCmd}`); + return scrambleResult; + } + + // Given --new-app-cmd-id and --new-app-cmd='publish' + if (options.newAppCmd === 'publish' && options.newAppCmdId) { + // Verify that stream ID is a valid GUID + if (!uuidValidate(options.newAppCmdId)) { + logger.error(`Invalid GUID in --new-app-cmd-id: ${options.newAppCmdId}`); + return scrambleResult; + } + + // Verify that stream exists, + const streamArray = await getStreamById(options.newAppCmdId, options); + if (streamArray === false || streamArray.length === 0) { + logger.error(`Stream with ID ${options.newAppCmdId} not found.`); + return scrambleResult; + } + } + + // Given --new-app-cmd-name and --new-app-cmd='publish' + if (options.newAppCmd === 'publish' && options.newAppCmdName) { + // Verify that stream exists, + const streamArray = await getStreamByName(options.newAppCmdName, options); + if (streamArray === false || streamArray.length === 0) { + logger.error(`Stream with name ${options.newAppCmdName} not found.`); + return scrambleResult; + } + } + + // Given --new-app-cmd-id and --new-app-cmd='replace' + if (options.newAppCmd === 'replace' && options.newAppCmdId) { + // Verify that app ID is a valid GUID + if (!uuidValidate(options.newAppCmdId)) { + logger.error(`Invalid GUID in --new-app-cmd-id: ${options.newAppCmdId}`); + return scrambleResult; + } + + // Verify that app exists + const appArray = await getAppById(options.newAppCmdId, options); + if (appArray === false) { + logger.error(`App with ID ${options.newAppCmdId} not found.`); + return scrambleResult; + } + } + + // Given --new-app-cmd-name and --new-app-cmd='replace' + if (options.newAppCmd === 'replace' && options.newAppCmdName) { + // Verify that app exists in singular + const appArray = await getAppByName(options.newAppCmdName, options); + if (appArray === false || appArray.length === 0) { + logger.error(`App with name ${options.newAppCmdName} not found.`); + return scrambleResult; + } + if (appArray.length > 1) { + logger.error(`More than one app with name ${options.newAppCmdName} found.`); + return scrambleResult; + } + } + // Session ID to use when connecting to the Qlik Sense server const sessionId = 'ctrlq'; @@ -58,11 +162,20 @@ export async function scrambleField(options) { const app = await global.openDoc(options.appId, '', '', '', false); logger.verbose(`Opened app ${options.appId}.`); - // Fields to be scrambled are availbel in array options.fieldName; - - if (options.fieldName.length === 0) { + // Fields to be scrambled are availble in array options.fieldName; + // If no fields are specified, no scrambling will be done + // options.fieldNams is an array of field names to be scrambled + // Verify it's an array + if (!options.fieldName || !Array.isArray(options.fieldName) || options?.fieldName?.length === 0) { // No fields specified logger.warn('No fields specified, no scrambling of data will be done, no new app will be created.'); + + // Close session + if ((await session.close()) === true) { + logger.verbose(`Closed session after scrambling fields in app ${options.appId} on host ${options.host}`); + } else { + logger.error(`Error closing session for app ${options.appId} on host ${options.host}`); + } } else { for (const field of options.fieldName) { // TODO make sure field exists before trying to scramble it @@ -86,103 +199,136 @@ export async function scrambleField(options) { logger.error(`Error closing session for app ${options.appId} on host ${options.host}`); } + // Add new app ID to result object + scrambleResult.newAppId = newAppId; + scrambleResult.status = 'success'; + + // ------------------------------------------------ // We now have a new app with scrambled data - // Proceed with other operations on the new app, e.g. publish, publish-replace, delete, etc. - if (options.newAppPublish) { - // Publish the new app to stream specified in options.newAppPublishStreamId or options.newAppPublishStreamName + // Proceed with other operations on the new app, e.g. publish, replace, delete, etc. - // Is stream ID or stream name specified? + if (options.newAppCmd === 'publish') { + // Publish the new app to stream specified in options.newAppCmdId or options.newAppCmdName + + // Is ID or name specified? let resultPublish; - if (options.newAppPublishStreamId) { + if (options.newAppCmdId) { // Publish to stream by stream ID - resultPublish = await publishApp(newAppId, options.newAppName, options.newAppPublishStreamId, options); - } else if (options.newAppPublishStreamName) { + resultPublish = await publishApp(newAppId, options.newAppName, options.newAppCmdId, options); + scrambleResult.status = 'success'; + } else if (options.newAppCmdName) { // Publish to stream by stream name // First look up stream ID by name // If there are multiple streams with the same name, report error and skip publishing // If no stream with the specified name is found, report error and skip publishing // If one stream is found, publish to that stream - const streamArray = await app.getStreamByName(options.newAppPublishStreamName, options); + const streamArray = await getStreamByName(options.newAppCmdName, options); if (streamArray.length === 1) { - logger.verbose(`Found stream with name "${options.newAppPublishStreamName}" with ID: ${streamArray[0].id}`); + logger.verbose(`Found stream with name "${options.newAppCmdName}" with ID: ${streamArray[0].id}`); resultPublish = await publishApp(newAppId, options.newAppName, streamArray[0].id, options); + scrambleResult.status = 'success'; } else if (streamArray.length > 1) { - logger.error(`More than one stream with name "${options.newAppPublishStreamName}" found. Skipping publish.`); + logger.error(`More than one stream with name "${options.newAppCmdName}" found. Skipping publish.`); + scrambleResult.status = 'error'; } else { - logger.error(`No stream with name "${options.newAppPublishStreamName}" found. Skipping publish.`); + logger.error(`No stream with name "${options.newAppCmdName}" found. Skipping publish.`); + scrambleResult.status = 'error'; } } if (resultPublish) { - logger.info( - `Published new app "${options.newAppName}" with app ID: ${newAppId} to stream "${options.newAppPublishStreamName}"` - ); + logger.info(`Published new app "${options.newAppName}" with app ID: ${newAppId} to stream "${options.newAppCmdName}"`); + scrambleResult.cmdDone = 'publish'; + scrambleResult.status = 'success'; } else { logger.error(`Error publishing new app "${options.newAppName}" with app ID: ${newAppId} to stream.`); + scrambleResult.status = 'error'; } - } - - if (options.newAppPublishReplace) { - // Publish-replace the new app with an existing published app + } else if (options.newAppCmd === 'replace') { + // Replace an existing app with the new, scrambled app // If app ID is specified, use that // If app name is specified, look up app ID by name - // If no app is found, report error and skip publish-replace - // If more than one app is found, report error and skip publish-replace - // If one app is found, publish-replace - let resultPublishReplace; - if (options.newAppPublishReplaceAppId) { - // Publish-replace by app ID - resultPublishReplace = await replaceApp(newAppId, options.newAppName, options.newAppPublishReplaceAppId, options); - } else if (options.newAppPublishReplaceAppName) { - // Publish-replace by app name + // If no app is found, report error and skip replace + // If more than one app is found, report error and skip replace + // If one app is found, replace + + let resultReplace; + if (options.newAppCmdId) { + // Replace by app ID + if (!options.force) { + const answer = await yesno({ + question: `Do you want to replace the existing app with app ID ${options.newAppCmdId} with the new, scrambled app? (y/n)`, + }); + + if (answer) { + resultReplace = await replaceApp(newAppId, options.newAppCmdId, options); + scrambleResult.status = 'success'; + } else { + logger.warn( + `Did not replace existing app with app ID ${options.newAppCmdId} with new, scrambled app "${options.newAppName}" with app ID ${newAppId}. The scrambled app is still available in My Work.` + ); + scrambleResult.status = 'aborted'; + } + } else { + resultReplace = await replaceApp(newAppId, options.newAppCmdId, options); + scrambleResult.status = 'success'; + } + } else if (options.newAppCmdName) { + // Replace by app name // First look up app ID by name - // If there are multiple apps with the same name, report error and skip publish-replace - // If no app with the specified name is found, report error and skip publish-replace - // If one app is found, publish-replace - const appArray = await app.getAppByName(options.newAppPublishReplaceAppName, options); + // If there are multiple apps with the same name, report error and skip replace + // If no app with the specified name is found, report error and skip replace + // If one app is found, replace + const appArray = await getAppByName(options.newAppCmdName, options); if (appArray.length === 1) { - logger.verbose(`Found app with name "${options.newAppPublishReplaceAppName}" with ID: ${appArray[0].id}`); - resultPublishReplace = await replaceApp(newAppId, options.newAppName, appArray[0].id, options); + logger.info(`Found app with name "${options.newAppCmdName}" with ID: ${appArray[0].id}`); + + if (!options.force) { + const answer = await yesno({ + question: `Do you want to replace the existing app with name "${options.newAppCmdName}" with the new, scrambled app? (y/n)`, + }); + + if (answer) { + resultReplace = await replaceApp(newAppId, appArray[0].id, options); + scrambleResult.status = 'success'; + } else { + logger.warn( + `Did not replace existing app with name "${options.newAppCmdName}" with new, scrambled app "${options.newAppName}" with app ID ${newAppId}. The scrambled app is still available in My Work.` + ); + scrambleResult.status = 'aborted'; + } + } else { + resultReplace = await replaceApp(newAppId, appArray[0].id, options); + scrambleResult.status = 'success'; + } } else if (appArray.length > 1) { - logger.error( - `More than one app with name "${options.newAppPublishReplaceAppName}" found. Skipping publish-replace.` - ); + logger.error(`More than one app with name "${options.newAppCmdName}" found. Skipping replace.`); + scrambleResult.status = 'error'; } else { - logger.error(`No app with name "${options.newAppPublishReplaceAppName}" found. Skipping publish-replace.`); + logger.error(`No app with name "${options.newAppCmdName}" found. Skipping replace.`); + scrambleResult.status = 'error'; } - } - } - if (options.newAppDeleteExistingUnpublished) { - // Delete any already existing apps with the same name as the new app - } - - if (options.newAppDelete) { - // Delete the new app after all other operations are done - // Ask user for confirmation unless --force option is set - if (options.force) { - await deleteAppById(newAppId, options); - logger.info(`Deleted new app "${options.newAppName}" with app ID: ${newAppId}`); - } else { - const answer = await yesno({ - question: `Do you want to delete the new app "${options.newAppName}" with app ID: ${newAppId}? (y/n)`, - }); - - if (answer) { - try { - await deleteAppById(newAppId, options); - logger.info(`Deleted new, scrambled app "${options.newAppName}" with app ID: ${newAppId}`); - } catch (err) { - catchLog(`Error deleting new app "${options.newAppName}" with app ID: ${newAppId}`, err); - } + if (resultReplace) { + logger.info( + `Replaced existing app "${options.newAppCmdName}" (app ID: ${appArray[0].id}) with new, scrambled app "${options.newAppName}" (app ID: ${newAppId})` + ); + scrambleResult.cmdDone = 'replace'; + scrambleResult.status = 'success'; } else { - logger.info(`Did not delete new app "${options.newAppName}" with app ID: ${newAppId}`); + logger.error( + `Error replacing existing app "${options.newAppCmdName}" with new, scrambled app "${options.newAppName}"` + ); + scrambleResult.status = 'error'; } } } } + + // Return the result of the scramble operation + return scrambleResult; } catch (err) { catchLog('Error in scrambleField', err); } diff --git a/src/lib/util/qseow/app.js b/src/lib/util/qseow/app.js index 8389b5f..e0972f4 100644 --- a/src/lib/util/qseow/app.js +++ b/src/lib/util/qseow/app.js @@ -81,6 +81,42 @@ export async function getApps(options, idArray, tagArray) { } } +// Function to get app(s) from QRS, given app name +// Returns array of zero or more app objects, or false if error +export async function getAppByName(appName, options) { + try { + logger.debug(`GET APP BY NAME: Starting get app from QSEoW for app name ${appName}`); + + // Did we get an app name? + if (!appName) { + logger.error(`GET APP BY NAME: No app name provided.`); + return false; + } + + // Set up connection to QRS + const axiosConfig = setupQrsConnection(options, { + method: 'get', + path: '/qrs/app/full', + queryParameters: [{ name: 'filter', value: encodeURI(`name eq '${appName}'`) }], + }); + + const result = await axios.request(axiosConfig); + logger.debug(`GET APP BY NAME: Result=${result.status}`); + + if (result.status === 200) { + const apps = JSON.parse(result.data); + logger.debug(`GET APP BY NAME: App details: ${apps}`); + + return apps; + } + + return false; + } catch (err) { + catchLog('GET APP BY NAME', err); + return false; + } +} + // Function to get app info from QRS, given app ID export async function getAppById(appId, optionsParam) { try { diff --git a/src/lib/util/qseow/assert-options.js b/src/lib/util/qseow/assert-options.js index 2940b4b..2344dcb 100644 --- a/src/lib/util/qseow/assert-options.js +++ b/src/lib/util/qseow/assert-options.js @@ -310,101 +310,56 @@ export const userActivityBucketsCustomPropertyAssertOptions = (options) => { export async function qseowScrambleFieldAssertOptions(options) { // Rules for options: - // - --new-app-publish: Publish the scrambled app to a stream. Optional. - // - If true, --new-app-publish-stream-id and --new-app-publish-stream-name options are used to determine which stream to publish to. Exactly one of those options must be present in this case. - // --new-app-publish-stream-id: Stream to which the scrambled app will be published. Default is ''. - // --new-app-publish-stream-name: Stream to which the scrambled app will be published. Default is ''. If more than one stream matches this name an error is returned. - // --new-app-publish-replace: Do a publish-replace using the scrambled app as source. Optional. - // - If true, The --new-app-publish-replace-app-id and --new-app-publish-replace-app-name options are used to determine which published app should be replaced. Exactly one of those two options must be present in this case. - // - --new-app-publish-replace-app-id: App ID for published app that will be replaced by newly created scrambled app. Default is ''. - // - --new-app-publish-replace-app-name: App name of published app that will be replaced by newly created scrambled app. Default is ''. If more than one published app matches this name an error is returned. - // - --new-app-delete-existing-unpublished: - // - If true, all unpublished apps (irrespective of owner) matching the app name passed in --new-app-name will be deleted before the source app is copied and scrambled. - // - --new-app-delete: Once all other activities are done, delete the newly created scrambled app. - // - --force: Do not ask for acknowledgment before deleting or replacing existing apps. - - // --new-app-publish: Publish the scrambled app to a stream. Optional. + // - --new-app-cmd: Either "publish" or "replace". Optional. If not specified, the new app will be placed in My Work. + // - If true, --new-app-cmd-id and --new-app-cmd-name options are used to determine which stream to publish to. Exactly one of those options must be present in this case. + // - --new-app-cmd-id: Stream/app to which the scrambled app will be published. Default is ''. + // - --new-app-cmd-name: Stream/app to which the scrambled app will be published. Default is ''. If more than one stream/app matches this name an error is returned. + // - --force: Do not ask for acknowledgment before replacing existing app. // Variable to keep track of whether options are valid let validOptions = true; - if (options.newAppPublish) { - // Neither of --new-app-publish-stream-id or --new-app-publish-stream-name are non-empty strings, exit - if (options.newAppPublishStreamId === '' && options.newAppPublishStreamName === '') { + if (options.newAppCmd === 'publish' || options.newAppCmd === 'replace') { + // Neither of --new-app-cmd-id or --new-app-cmd-name are empty strings, exit + if (options.newAppCmdId === '' && options.newAppCmdName === '') { logger.error( - 'When --new-app-publish is true, exactly one of --new-app-publish-stream-id or --new-app-publish-stream-name must be present.' + 'When --new-app-cmd is either "publish" or "replace", exactly one of --new-app-cmd-id and --new-app-cmd-name must be present.' ); validOptions = false; } - // If both --new-app-publish-stream-id and --new-app-publish-stream-name are non-empty strings, exit - if (options.newAppPublishStreamId !== '' && options.newAppPublishStreamName !== '') { - logger.error( - 'When --new-app-publish is true, exactly one of --new-app-publish-stream-id or --new-app-publish-stream-name must be present.' - ); - validOptions = false; - } - - // If --new-app-publish-stream-id is a non-empty string, it must be a valid uuid - if (options.newAppPublishStreamId !== '' && !uuidValidate(options.newAppPublishStreamId)) { - logger.error(`Invalid format of stream ID "${options.newAppPublishStreamId}".`); + // If both --new-app-cmd-id and --new-app-cmd-name are non-empty strings, exit + if (options.newAppCmdId !== '' && options.newAppCmdName !== '') { + logger.error('When --new-app-cmd is true, exactly one of --new-app-cmd-id or --new-app-cmd-name must be present.'); validOptions = false; } - // If --new-app-publish-stream-name is a non-empty string, it must not contain any special characters - if (options.newAppPublishStreamName !== '' && !/^[a-zA-Z0-9_]+$/.test(options.newAppPublishStreamName)) { - logger.error(`Invalid stream name "${options.newAppPublishStreamName}". Only letters, numbers and underscores are allowed.`); + // If --new-app-cmd-id is a non-empty string, it must be a valid uuid + if (options.newAppCmdId !== '' && !uuidValidate(options.newAppCmdId)) { + logger.error(`Invalid format of --new-app-cmd-id (not a valid ID): "${options.newAppCmdId}".`); validOptions = false; } - // If --new-app-publish-stream-name is a non-empty string, it must exist in the Qlik Sense environment - if (options.newAppPublishStreamName !== '') { + // If --new-app-cmd-name is a non-empty string, it must exist in the Qlik Sense environment + if (options.newAppCmdName !== '') { // TODO: Implement this check - // const stream = await global.getStream(options.newAppPublishStreamName); + // const stream = await global.getStream(options.newAppCmdStreamName); // if (stream === null) { - // logger.error(`Stream "${options.newAppPublishStreamName}" does not exist in the Qlik Sense environment.`); + // logger.error(`Stream "${options.newAppCmdStreamName}" does not exist in the Qlik Sense environment.`); // validOptions = false; // } } - } - - // --new-app-publish-replace: Do a publish-replace using the scrambled app as source. Optional. - if (options.newAppPublishReplace) { - // Neither of --new-app-publish-replace-app-id or --new-app-publish-replace-app-name are non-empty strings, exit - if (options.newAppPublishReplaceAppId === '' && options.newAppPublishReplaceAppName === '') { - logger.error( - 'When --new-app-publish-replace is true, exactly one of --new-app-publish-replace-app-id or --new-app-publish-replace-app-name must be present.' - ); - validOptions = false; - } - // If both --new-app-publish-replace-app-id and --new-app-publish-replace-app-name are non-empty strings, exit - if (options.newAppPublishReplaceAppId !== '' && options.newAppPublishReplaceAppName !== '') { - logger.error( - 'When --new-app-publish-replace is true, exactly one of --new-app-publish-replace-app-id or --new-app-publish-replace-app-name must be present.' - ); - validOptions = false; - } - - // If --new-app-publish-replace-app-id is a non-empty string, it must be a valid uuid - if (options.newAppPublishReplaceAppId !== '' && !uuidValidate(options.newAppPublishReplaceAppId)) { - logger.error(`Invalid format of app ID "${options.newAppPublishReplaceAppId}".`); - validOptions = false; - } - - // If --new-app-publish-replace-app-name is a non-empty string, that app must exist in the Qlik Sense environment and be published - if (options.newAppPublishReplaceAppName !== '') { + // If --new-app-cmd-id is a non-empty string, it must exist in the Qlik Sense environment + if (options.newAppCmdId !== '') { // TODO: Implement this check } } - // --new-app-delete-existing-unpublished: If true, all unpublished apps (irrespective of owner) matching the app name passed in --new-app-name will be deleted before the source app is copied and scrambled. - if (options.newAppDeleteExistingUnpublished) { - // --new-app-delete-existing-unpublished is true, but --new-app-name is not a non-empty string - if (options.newAppName === '') { - logger.error('When --new-app-delete-existing-unpublished is true, --new-app-name must be a non-empty string.'); - validOptions = false; - } + // If publishing to a stream, --new-app-cmd-name must be a non-empty string + if (options.newAppCmd === 'publish' && options.newAppCmdName === '') { + logger.error('When --new-app-cmd is "publish", --new-app-cmd-name must be a non-empty string.'); + validOptions = false; } if (validOptions === false) { diff --git a/src/lib/util/qseow/stream.js b/src/lib/util/qseow/stream.js index aec1160..aa802ad 100644 --- a/src/lib/util/qseow/stream.js +++ b/src/lib/util/qseow/stream.js @@ -4,7 +4,7 @@ import { logger } from '../../../globals.js'; import { setupQrsConnection } from './qrs.js'; import { catchLog } from '../log.js'; -// Function to get stream(s) from QRS, given a stram name +// Function to get stream(s) from QRS, given a stream name // Parameters: // - streamName: Name of stream to get // - options: Command line options @@ -46,3 +46,46 @@ export async function getStreamByName(streamName, options) { return false; } } + +// Function to get stream(s) from QRS, given a stream ID +// Parameters: +// - streamId: ID of stream to get +// - options: Command line options +// +// Returns: +// - Array of zero or more stream objects. +// - false if error +export async function getStreamById(streamId, options) { + try { + logger.debug(`GET STREAM BY ID: Starting get stream by ID from QSEoW for stream ${streamId}`); + + // Did we get a stream ID? + if (!streamId) { + logger.error(`GET STREAM BY ID: No stream ID provided.`); + return false; + } + + // Set up connection to QRS + const axiosConfig = setupQrsConnection(options, { + method: 'get', + path: `/qrs/stream/full`, + queryParameters: [{ name: 'filter', value: `id eq ${streamId}` }], + }); + + const result = await axios.request(axiosConfig); + logger.debug(`GET STREAM BY ID: Result=${result.status}`); + + if (result.status === 200) { + const streamArray = JSON.parse(result.data); + logger.debug(`GET STREAM BY ID: Stream details: ${streamArray}`); + logger.verbose(`Found ${streamArray.length} streams with ID ${streamId}`); + + return streamArray; + } + + return false; + } catch (err) { + catchLog('GET STREAM BY ID', err); + return false; + } +}