diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md index 28ddbd896..b48a69a76 100644 --- a/packages/cli/CHANGELOG.md +++ b/packages/cli/CHANGELOG.md @@ -1,6 +1,11 @@ # Change Log All notable changes to the Zowe CLI package will be documented in this file. + +## Recent Changes + +- BugFix: Users were not warned when copying partitioned data sets with identical member names. Now, the user is prompted to confirm before continuing the copy operation to avoid potential data loss. [#2349] (https://github.com/zowe/zowe-cli/issues/2349) + ## `8.14.0` - Enhancement: Added the ability to see secure properties when running `zowe config list` when the `ZOWE_SHOW_SECURE_ARGS` environment variable is set to `true`. [#2259](https://github.com/zowe/zowe-cli/issues/2259) @@ -935,7 +940,7 @@ LTS Breaking: Removed the following previously deprecated items: [#1981](https:/ ## `6.25.0` -- Enhancement: Added a `--replace` option to the `zowe zos-files copy data-set` command. Use this option if you want to replace like-named members in the target data set. [#808](https://github.com/zowe/zowe-cli/issues/808) +- Enhancement: Added a `--replace` option to the `zowe zos-files copy data-set` command. Use this option if you want to replace members with identical names in the target data set. [#808](https://github.com/zowe/zowe-cli/issues/808) - Enhancement: Improved a cryptic error message that was shown if TSO address space failed to start for the `zowe zos-tso issue command` command. [#28](https://github.com/zowe/zowe-cli/issues/28) - Bugfix: Removed "[object Object]" text that appeared in some error messages. The proper text "Imperative API Error" is now displayed. [#836](https://github.com/zowe/zowe-cli/pull/836) diff --git a/packages/cli/__tests__/zosfiles/__integration__/copy/ds/__snapshots__/cli.files.copy.ds.integration.test.ts.snap b/packages/cli/__tests__/zosfiles/__integration__/copy/ds/__snapshots__/cli.files.copy.ds.integration.test.ts.snap index 649eac42b..a250cb8c9 100644 --- a/packages/cli/__tests__/zosfiles/__integration__/copy/ds/__snapshots__/cli.files.copy.ds.integration.test.ts.snap +++ b/packages/cli/__tests__/zosfiles/__integration__/copy/ds/__snapshots__/cli.files.copy.ds.integration.test.ts.snap @@ -34,13 +34,13 @@ exports[`Copy Data Set should display the help 1`] = ` --replace | --rep (boolean) - Specify this option as true if you wish to replace like-named members in the - target data set + Specify this option as true if you wish to replace members with identical names + in the target data set --safe-replace | --safe-rep | --sr (boolean) - Specify this option as true if you wish to replace like-named members or the - content of the target data set. This option will prompt to confirm. + Specify this option as true if you wish to replace members with identical names + or the content of the target data set. This option will prompt to confirm. --response-timeout | --rto (number) @@ -162,7 +162,7 @@ exports[`Copy Data Set should display the help 1`] = ` $ zowe zos-files copy data-set \\"USER.FROM.SET(mem1)\\" \\"USER.TO.SET\\" - Copy the data set named 'USER.FROM.SET' to the data set - named 'USER.TO.SET' and replace like-named members: + named 'USER.TO.SET' and replace members with identical names: $ zowe zos-files copy data-set \\"USER.FROM.SET\\" \\"USER.TO.SET\\" --replace @@ -180,8 +180,8 @@ exports[`Copy Data Set should display the help in json format 1`] = ` \\"success\\": true, \\"exitCode\\": 0, \\"message\\": \\"The help was constructed for command: data-set.\\", - \\"stdout\\": \\"\\\\n COMMAND NAME\\\\n ------------\\\\n\\\\n data-set | ds\\\\n\\\\n DESCRIPTION\\\\n -----------\\\\n\\\\n Copy a data set/partitioned data set to another data set/partitioned data set.\\\\n\\\\n USAGE\\\\n -----\\\\n\\\\n zowe zos-files copy data-set [options]\\\\n\\\\n POSITIONAL ARGUMENTS\\\\n --------------------\\\\n\\\\n fromDataSetName\\\\t\\\\t (string)\\\\n\\\\n The name of the data set that you want to copy from\\\\n\\\\n toDataSetName\\\\t\\\\t (string)\\\\n\\\\n The name of the data set that you want to copy to\\\\n\\\\n OPTIONS\\\\n -------\\\\n\\\\n --replace | --rep (boolean)\\\\n\\\\n Specify this option as true if you wish to replace like-named members in the\\\\n target data set\\\\n\\\\n --safe-replace | --safe-rep | --sr (boolean)\\\\n\\\\n Specify this option as true if you wish to replace like-named members or the\\\\n content of the target data set. This option will prompt to confirm.\\\\n\\\\n --response-timeout | --rto (number)\\\\n\\\\n The maximum amount of time in seconds the z/OSMF Files TSO servlet should run\\\\n before returning a response. Any request exceeding this amount of time will be\\\\n terminated and return an error. Allowed values: 5 - 600\\\\n\\\\n ZOSMF CONNECTION OPTIONS\\\\n ------------------------\\\\n\\\\n --host | -H (string)\\\\n\\\\n The z/OSMF server host name.\\\\n\\\\n --port | -P (number)\\\\n\\\\n The z/OSMF server port.\\\\n\\\\n Default value: 443\\\\n\\\\n --user | -u (string)\\\\n\\\\n Mainframe (z/OSMF) user name, which can be the same as your TSO login.\\\\n\\\\n --password | --pass | --pw (string)\\\\n\\\\n Mainframe (z/OSMF) password, which can be the same as your TSO password.\\\\n\\\\n --reject-unauthorized | --ru (boolean)\\\\n\\\\n Reject self-signed certificates.\\\\n\\\\n Default value: true\\\\n\\\\n --base-path | --bp (string)\\\\n\\\\n The base path for your API mediation layer instance. Specify this option to\\\\n prepend the base path to all z/OSMF resources when making REST requests. Do not\\\\n specify this option if you are not using an API mediation layer.\\\\n\\\\n --protocol (string)\\\\n\\\\n The protocol used (HTTP or HTTPS)\\\\n\\\\n Default value: https\\\\n Allowed values: http, https\\\\n\\\\n --cert-file (local file path)\\\\n\\\\n The file path to a certificate file to use for authentication\\\\n\\\\n --cert-key-file (local file path)\\\\n\\\\n The file path to a certificate key file to use for authentication\\\\n\\\\n PROFILE OPTIONS\\\\n ---------------\\\\n\\\\n --zosmf-profile | --zosmf-p (string)\\\\n\\\\n The name of a (zosmf) profile to load for this command execution.\\\\n\\\\n --base-profile | --base-p (string)\\\\n\\\\n The name of a (base) profile to load for this command execution.\\\\n\\\\n BASE CONNECTION OPTIONS\\\\n -----------------------\\\\n\\\\n --token-type | --tt (string)\\\\n\\\\n The type of token to get and use for the API. Omit this option to use the\\\\n default token type, which is provided by 'zowe auth login'.\\\\n\\\\n --token-value | --tv (string)\\\\n\\\\n The value of the token to pass to the API.\\\\n\\\\n GLOBAL OPTIONS\\\\n --------------\\\\n\\\\n --show-inputs-only (boolean)\\\\n\\\\n Show command inputs and do not run the command\\\\n\\\\n --response-format-json | --rfj (boolean)\\\\n\\\\n Produce JSON formatted data from a command\\\\n\\\\n --help | -h (boolean)\\\\n\\\\n Display help text\\\\n\\\\n --help-web | --hw (boolean)\\\\n\\\\n Display HTML help in browser\\\\n\\\\n EXAMPLES\\\\n --------\\\\n\\\\n - Copy the data set named 'USER.FROM.SET' to the data set\\\\n named 'USER.TO.SET':\\\\n\\\\n $ zowe zos-files copy data-set \\\\\\"USER.FROM.SET\\\\\\" \\\\\\"USER.TO.SET\\\\\\"\\\\n\\\\n - Copy the data set member named 'USER.FROM.SET(MEM1)' to the\\\\n data set member named 'USER.TO.SET(MEM2)':\\\\n\\\\n $ zowe zos-files copy data-set \\\\\\"USER.FROM.SET(mem1)\\\\\\" \\\\\\"USER.TO.SET(mem2)\\\\\\"\\\\n\\\\n - Copy the data set named 'USER.FROM.SET' to the data set\\\\n member named 'USER.TO.SET(MEM2)':\\\\n\\\\n $ zowe zos-files copy data-set \\\\\\"USER.FROM.SET\\\\\\" \\\\\\"USER.TO.SET(mem2)\\\\\\"\\\\n\\\\n - Copy the data set member named 'USER.FROM.SET(MEM1)' to the\\\\n data set named 'USER.TO.SET':\\\\n\\\\n $ zowe zos-files copy data-set \\\\\\"USER.FROM.SET(mem1)\\\\\\" \\\\\\"USER.TO.SET\\\\\\"\\\\n\\\\n - Copy the data set named 'USER.FROM.SET' to the data set\\\\n named 'USER.TO.SET' and replace like-named members:\\\\n\\\\n $ zowe zos-files copy data-set \\\\\\"USER.FROM.SET\\\\\\" \\\\\\"USER.TO.SET\\\\\\" --replace\\\\n\\\\n - Copy the partitioned data set named 'TEST.PDS1' to the\\\\n partitioned data set named 'TEST.PDS2':\\\\n\\\\n $ zowe zos-files copy data-set \\\\\\"USER.FROM.SET\\\\\\" \\\\\\"USER.TO.SET\\\\\\" --safe-replace\\\\n\\\\n\\", + \\"stdout\\": \\"\\\\n COMMAND NAME\\\\n ------------\\\\n\\\\n data-set | ds\\\\n\\\\n DESCRIPTION\\\\n -----------\\\\n\\\\n Copy a data set/partitioned data set to another data set/partitioned data set.\\\\n\\\\n USAGE\\\\n -----\\\\n\\\\n zowe zos-files copy data-set [options]\\\\n\\\\n POSITIONAL ARGUMENTS\\\\n --------------------\\\\n\\\\n fromDataSetName\\\\t\\\\t (string)\\\\n\\\\n The name of the data set that you want to copy from\\\\n\\\\n toDataSetName\\\\t\\\\t (string)\\\\n\\\\n The name of the data set that you want to copy to\\\\n\\\\n OPTIONS\\\\n -------\\\\n\\\\n --replace | --rep (boolean)\\\\n\\\\n Specify this option as true if you wish to replace members with identical names\\\\n in the target data set\\\\n\\\\n --safe-replace | --safe-rep | --sr (boolean)\\\\n\\\\n Specify this option as true if you wish to replace members with identical names\\\\n or the content of the target data set. This option will prompt to confirm.\\\\n\\\\n --response-timeout | --rto (number)\\\\n\\\\n The maximum amount of time in seconds the z/OSMF Files TSO servlet should run\\\\n before returning a response. Any request exceeding this amount of time will be\\\\n terminated and return an error. Allowed values: 5 - 600\\\\n\\\\n ZOSMF CONNECTION OPTIONS\\\\n ------------------------\\\\n\\\\n --host | -H (string)\\\\n\\\\n The z/OSMF server host name.\\\\n\\\\n --port | -P (number)\\\\n\\\\n The z/OSMF server port.\\\\n\\\\n Default value: 443\\\\n\\\\n --user | -u (string)\\\\n\\\\n Mainframe (z/OSMF) user name, which can be the same as your TSO login.\\\\n\\\\n --password | --pass | --pw (string)\\\\n\\\\n Mainframe (z/OSMF) password, which can be the same as your TSO password.\\\\n\\\\n --reject-unauthorized | --ru (boolean)\\\\n\\\\n Reject self-signed certificates.\\\\n\\\\n Default value: true\\\\n\\\\n --base-path | --bp (string)\\\\n\\\\n The base path for your API mediation layer instance. Specify this option to\\\\n prepend the base path to all z/OSMF resources when making REST requests. Do not\\\\n specify this option if you are not using an API mediation layer.\\\\n\\\\n --protocol (string)\\\\n\\\\n The protocol used (HTTP or HTTPS)\\\\n\\\\n Default value: https\\\\n Allowed values: http, https\\\\n\\\\n --cert-file (local file path)\\\\n\\\\n The file path to a certificate file to use for authentication\\\\n\\\\n --cert-key-file (local file path)\\\\n\\\\n The file path to a certificate key file to use for authentication\\\\n\\\\n PROFILE OPTIONS\\\\n ---------------\\\\n\\\\n --zosmf-profile | --zosmf-p (string)\\\\n\\\\n The name of a (zosmf) profile to load for this command execution.\\\\n\\\\n --base-profile | --base-p (string)\\\\n\\\\n The name of a (base) profile to load for this command execution.\\\\n\\\\n BASE CONNECTION OPTIONS\\\\n -----------------------\\\\n\\\\n --token-type | --tt (string)\\\\n\\\\n The type of token to get and use for the API. Omit this option to use the\\\\n default token type, which is provided by 'zowe auth login'.\\\\n\\\\n --token-value | --tv (string)\\\\n\\\\n The value of the token to pass to the API.\\\\n\\\\n GLOBAL OPTIONS\\\\n --------------\\\\n\\\\n --show-inputs-only (boolean)\\\\n\\\\n Show command inputs and do not run the command\\\\n\\\\n --response-format-json | --rfj (boolean)\\\\n\\\\n Produce JSON formatted data from a command\\\\n\\\\n --help | -h (boolean)\\\\n\\\\n Display help text\\\\n\\\\n --help-web | --hw (boolean)\\\\n\\\\n Display HTML help in browser\\\\n\\\\n EXAMPLES\\\\n --------\\\\n\\\\n - Copy the data set named 'USER.FROM.SET' to the data set\\\\n named 'USER.TO.SET':\\\\n\\\\n $ zowe zos-files copy data-set \\\\\\"USER.FROM.SET\\\\\\" \\\\\\"USER.TO.SET\\\\\\"\\\\n\\\\n - Copy the data set member named 'USER.FROM.SET(MEM1)' to the\\\\n data set member named 'USER.TO.SET(MEM2)':\\\\n\\\\n $ zowe zos-files copy data-set \\\\\\"USER.FROM.SET(mem1)\\\\\\" \\\\\\"USER.TO.SET(mem2)\\\\\\"\\\\n\\\\n - Copy the data set named 'USER.FROM.SET' to the data set\\\\n member named 'USER.TO.SET(MEM2)':\\\\n\\\\n $ zowe zos-files copy data-set \\\\\\"USER.FROM.SET\\\\\\" \\\\\\"USER.TO.SET(mem2)\\\\\\"\\\\n\\\\n - Copy the data set member named 'USER.FROM.SET(MEM1)' to the\\\\n data set named 'USER.TO.SET':\\\\n\\\\n $ zowe zos-files copy data-set \\\\\\"USER.FROM.SET(mem1)\\\\\\" \\\\\\"USER.TO.SET\\\\\\"\\\\n\\\\n - Copy the data set named 'USER.FROM.SET' to the data set\\\\n named 'USER.TO.SET' and replace members with identical names:\\\\n\\\\n $ zowe zos-files copy data-set \\\\\\"USER.FROM.SET\\\\\\" \\\\\\"USER.TO.SET\\\\\\" --replace\\\\n\\\\n - Copy the partitioned data set named 'TEST.PDS1' to the\\\\n partitioned data set named 'TEST.PDS2':\\\\n\\\\n $ zowe zos-files copy data-set \\\\\\"USER.FROM.SET\\\\\\" \\\\\\"USER.TO.SET\\\\\\" --safe-replace\\\\n\\\\n\\", \\"stderr\\": \\"\\", - \\"data\\": \\"\\\\n COMMAND NAME\\\\n ------------\\\\n\\\\n data-set | ds\\\\n\\\\n DESCRIPTION\\\\n -----------\\\\n\\\\n Copy a data set/partitioned data set to another data set/partitioned data set.\\\\n\\\\n USAGE\\\\n -----\\\\n\\\\n zowe zos-files copy data-set [options]\\\\n\\\\n POSITIONAL ARGUMENTS\\\\n --------------------\\\\n\\\\n fromDataSetName\\\\t\\\\t (string)\\\\n\\\\n The name of the data set that you want to copy from\\\\n\\\\n toDataSetName\\\\t\\\\t (string)\\\\n\\\\n The name of the data set that you want to copy to\\\\n\\\\n OPTIONS\\\\n -------\\\\n\\\\n --replace | --rep (boolean)\\\\n\\\\n Specify this option as true if you wish to replace like-named members in the\\\\n target data set\\\\n\\\\n --safe-replace | --safe-rep | --sr (boolean)\\\\n\\\\n Specify this option as true if you wish to replace like-named members or the\\\\n content of the target data set. This option will prompt to confirm.\\\\n\\\\n --response-timeout | --rto (number)\\\\n\\\\n The maximum amount of time in seconds the z/OSMF Files TSO servlet should run\\\\n before returning a response. Any request exceeding this amount of time will be\\\\n terminated and return an error. Allowed values: 5 - 600\\\\n\\\\n ZOSMF CONNECTION OPTIONS\\\\n ------------------------\\\\n\\\\n --host | -H (string)\\\\n\\\\n The z/OSMF server host name.\\\\n\\\\n --port | -P (number)\\\\n\\\\n The z/OSMF server port.\\\\n\\\\n Default value: 443\\\\n\\\\n --user | -u (string)\\\\n\\\\n Mainframe (z/OSMF) user name, which can be the same as your TSO login.\\\\n\\\\n --password | --pass | --pw (string)\\\\n\\\\n Mainframe (z/OSMF) password, which can be the same as your TSO password.\\\\n\\\\n --reject-unauthorized | --ru (boolean)\\\\n\\\\n Reject self-signed certificates.\\\\n\\\\n Default value: true\\\\n\\\\n --base-path | --bp (string)\\\\n\\\\n The base path for your API mediation layer instance. Specify this option to\\\\n prepend the base path to all z/OSMF resources when making REST requests. Do not\\\\n specify this option if you are not using an API mediation layer.\\\\n\\\\n --protocol (string)\\\\n\\\\n The protocol used (HTTP or HTTPS)\\\\n\\\\n Default value: https\\\\n Allowed values: http, https\\\\n\\\\n --cert-file (local file path)\\\\n\\\\n The file path to a certificate file to use for authentication\\\\n\\\\n --cert-key-file (local file path)\\\\n\\\\n The file path to a certificate key file to use for authentication\\\\n\\\\n PROFILE OPTIONS\\\\n ---------------\\\\n\\\\n --zosmf-profile | --zosmf-p (string)\\\\n\\\\n The name of a (zosmf) profile to load for this command execution.\\\\n\\\\n --base-profile | --base-p (string)\\\\n\\\\n The name of a (base) profile to load for this command execution.\\\\n\\\\n BASE CONNECTION OPTIONS\\\\n -----------------------\\\\n\\\\n --token-type | --tt (string)\\\\n\\\\n The type of token to get and use for the API. Omit this option to use the\\\\n default token type, which is provided by 'zowe auth login'.\\\\n\\\\n --token-value | --tv (string)\\\\n\\\\n The value of the token to pass to the API.\\\\n\\\\n GLOBAL OPTIONS\\\\n --------------\\\\n\\\\n --show-inputs-only (boolean)\\\\n\\\\n Show command inputs and do not run the command\\\\n\\\\n --response-format-json | --rfj (boolean)\\\\n\\\\n Produce JSON formatted data from a command\\\\n\\\\n --help | -h (boolean)\\\\n\\\\n Display help text\\\\n\\\\n --help-web | --hw (boolean)\\\\n\\\\n Display HTML help in browser\\\\n\\\\n EXAMPLES\\\\n --------\\\\n\\\\n - Copy the data set named 'USER.FROM.SET' to the data set\\\\n named 'USER.TO.SET':\\\\n\\\\n $ zowe zos-files copy data-set \\\\\\"USER.FROM.SET\\\\\\" \\\\\\"USER.TO.SET\\\\\\"\\\\n\\\\n - Copy the data set member named 'USER.FROM.SET(MEM1)' to the\\\\n data set member named 'USER.TO.SET(MEM2)':\\\\n\\\\n $ zowe zos-files copy data-set \\\\\\"USER.FROM.SET(mem1)\\\\\\" \\\\\\"USER.TO.SET(mem2)\\\\\\"\\\\n\\\\n - Copy the data set named 'USER.FROM.SET' to the data set\\\\n member named 'USER.TO.SET(MEM2)':\\\\n\\\\n $ zowe zos-files copy data-set \\\\\\"USER.FROM.SET\\\\\\" \\\\\\"USER.TO.SET(mem2)\\\\\\"\\\\n\\\\n - Copy the data set member named 'USER.FROM.SET(MEM1)' to the\\\\n data set named 'USER.TO.SET':\\\\n\\\\n $ zowe zos-files copy data-set \\\\\\"USER.FROM.SET(mem1)\\\\\\" \\\\\\"USER.TO.SET\\\\\\"\\\\n\\\\n - Copy the data set named 'USER.FROM.SET' to the data set\\\\n named 'USER.TO.SET' and replace like-named members:\\\\n\\\\n $ zowe zos-files copy data-set \\\\\\"USER.FROM.SET\\\\\\" \\\\\\"USER.TO.SET\\\\\\" --replace\\\\n\\\\n - Copy the partitioned data set named 'TEST.PDS1' to the\\\\n partitioned data set named 'TEST.PDS2':\\\\n\\\\n $ zowe zos-files copy data-set \\\\\\"USER.FROM.SET\\\\\\" \\\\\\"USER.TO.SET\\\\\\" --safe-replace\\\\n\\\\n\\" + \\"data\\": \\"\\\\n COMMAND NAME\\\\n ------------\\\\n\\\\n data-set | ds\\\\n\\\\n DESCRIPTION\\\\n -----------\\\\n\\\\n Copy a data set/partitioned data set to another data set/partitioned data set.\\\\n\\\\n USAGE\\\\n -----\\\\n\\\\n zowe zos-files copy data-set [options]\\\\n\\\\n POSITIONAL ARGUMENTS\\\\n --------------------\\\\n\\\\n fromDataSetName\\\\t\\\\t (string)\\\\n\\\\n The name of the data set that you want to copy from\\\\n\\\\n toDataSetName\\\\t\\\\t (string)\\\\n\\\\n The name of the data set that you want to copy to\\\\n\\\\n OPTIONS\\\\n -------\\\\n\\\\n --replace | --rep (boolean)\\\\n\\\\n Specify this option as true if you wish to replace members with identical names\\\\n in the target data set\\\\n\\\\n --safe-replace | --safe-rep | --sr (boolean)\\\\n\\\\n Specify this option as true if you wish to replace members with identical names\\\\n or the content of the target data set. This option will prompt to confirm.\\\\n\\\\n --response-timeout | --rto (number)\\\\n\\\\n The maximum amount of time in seconds the z/OSMF Files TSO servlet should run\\\\n before returning a response. Any request exceeding this amount of time will be\\\\n terminated and return an error. Allowed values: 5 - 600\\\\n\\\\n ZOSMF CONNECTION OPTIONS\\\\n ------------------------\\\\n\\\\n --host | -H (string)\\\\n\\\\n The z/OSMF server host name.\\\\n\\\\n --port | -P (number)\\\\n\\\\n The z/OSMF server port.\\\\n\\\\n Default value: 443\\\\n\\\\n --user | -u (string)\\\\n\\\\n Mainframe (z/OSMF) user name, which can be the same as your TSO login.\\\\n\\\\n --password | --pass | --pw (string)\\\\n\\\\n Mainframe (z/OSMF) password, which can be the same as your TSO password.\\\\n\\\\n --reject-unauthorized | --ru (boolean)\\\\n\\\\n Reject self-signed certificates.\\\\n\\\\n Default value: true\\\\n\\\\n --base-path | --bp (string)\\\\n\\\\n The base path for your API mediation layer instance. Specify this option to\\\\n prepend the base path to all z/OSMF resources when making REST requests. Do not\\\\n specify this option if you are not using an API mediation layer.\\\\n\\\\n --protocol (string)\\\\n\\\\n The protocol used (HTTP or HTTPS)\\\\n\\\\n Default value: https\\\\n Allowed values: http, https\\\\n\\\\n --cert-file (local file path)\\\\n\\\\n The file path to a certificate file to use for authentication\\\\n\\\\n --cert-key-file (local file path)\\\\n\\\\n The file path to a certificate key file to use for authentication\\\\n\\\\n PROFILE OPTIONS\\\\n ---------------\\\\n\\\\n --zosmf-profile | --zosmf-p (string)\\\\n\\\\n The name of a (zosmf) profile to load for this command execution.\\\\n\\\\n --base-profile | --base-p (string)\\\\n\\\\n The name of a (base) profile to load for this command execution.\\\\n\\\\n BASE CONNECTION OPTIONS\\\\n -----------------------\\\\n\\\\n --token-type | --tt (string)\\\\n\\\\n The type of token to get and use for the API. Omit this option to use the\\\\n default token type, which is provided by 'zowe auth login'.\\\\n\\\\n --token-value | --tv (string)\\\\n\\\\n The value of the token to pass to the API.\\\\n\\\\n GLOBAL OPTIONS\\\\n --------------\\\\n\\\\n --show-inputs-only (boolean)\\\\n\\\\n Show command inputs and do not run the command\\\\n\\\\n --response-format-json | --rfj (boolean)\\\\n\\\\n Produce JSON formatted data from a command\\\\n\\\\n --help | -h (boolean)\\\\n\\\\n Display help text\\\\n\\\\n --help-web | --hw (boolean)\\\\n\\\\n Display HTML help in browser\\\\n\\\\n EXAMPLES\\\\n --------\\\\n\\\\n - Copy the data set named 'USER.FROM.SET' to the data set\\\\n named 'USER.TO.SET':\\\\n\\\\n $ zowe zos-files copy data-set \\\\\\"USER.FROM.SET\\\\\\" \\\\\\"USER.TO.SET\\\\\\"\\\\n\\\\n - Copy the data set member named 'USER.FROM.SET(MEM1)' to the\\\\n data set member named 'USER.TO.SET(MEM2)':\\\\n\\\\n $ zowe zos-files copy data-set \\\\\\"USER.FROM.SET(mem1)\\\\\\" \\\\\\"USER.TO.SET(mem2)\\\\\\"\\\\n\\\\n - Copy the data set named 'USER.FROM.SET' to the data set\\\\n member named 'USER.TO.SET(MEM2)':\\\\n\\\\n $ zowe zos-files copy data-set \\\\\\"USER.FROM.SET\\\\\\" \\\\\\"USER.TO.SET(mem2)\\\\\\"\\\\n\\\\n - Copy the data set member named 'USER.FROM.SET(MEM1)' to the\\\\n data set named 'USER.TO.SET':\\\\n\\\\n $ zowe zos-files copy data-set \\\\\\"USER.FROM.SET(mem1)\\\\\\" \\\\\\"USER.TO.SET\\\\\\"\\\\n\\\\n - Copy the data set named 'USER.FROM.SET' to the data set\\\\n named 'USER.TO.SET' and replace members with identical names:\\\\n\\\\n $ zowe zos-files copy data-set \\\\\\"USER.FROM.SET\\\\\\" \\\\\\"USER.TO.SET\\\\\\" --replace\\\\n\\\\n - Copy the partitioned data set named 'TEST.PDS1' to the\\\\n partitioned data set named 'TEST.PDS2':\\\\n\\\\n $ zowe zos-files copy data-set \\\\\\"USER.FROM.SET\\\\\\" \\\\\\"USER.TO.SET\\\\\\" --safe-replace\\\\n\\\\n\\" }" `; diff --git a/packages/cli/__tests__/zosfiles/__unit__/copy/ds/Ds.handler.unit.test.ts b/packages/cli/__tests__/zosfiles/__unit__/copy/ds/Ds.handler.unit.test.ts index 2975ad862..a847dff06 100644 --- a/packages/cli/__tests__/zosfiles/__unit__/copy/ds/Ds.handler.unit.test.ts +++ b/packages/cli/__tests__/zosfiles/__unit__/copy/ds/Ds.handler.unit.test.ts @@ -50,7 +50,7 @@ describe("DsHandler", () => { }, response: { - console: { promptFn: jest.fn() } + console: { promptFn: jest.fn(), promptForIdenticalNamedMembers: jest.fn() } } }; @@ -68,7 +68,8 @@ describe("DsHandler", () => { "replace": commandParameters.arguments.replace, "responseTimeout": commandParameters.arguments.responseTimeout, "safeReplace": commandParameters.arguments.safeReplace, - "promptFn": expect.any(Function) + "promptFn": expect.any(Function), + "promptForIdenticalNamedMembers": expect.any(Function) } ); expect(response).toBe(defaultReturn); @@ -98,7 +99,7 @@ describe("DsHandler", () => { responseTimeout }, response: { - console: { promptFn: jest.fn() } + console: { promptFn: jest.fn(), promptForIdenticalNamedMembers: jest.fn() } } }; @@ -116,7 +117,8 @@ describe("DsHandler", () => { "replace": commandParameters.arguments.replace, "responseTimeout": commandParameters.arguments.responseTimeout, "safeReplace": commandParameters.arguments.safeReplace, - "promptFn": expect.any(Function) + "promptFn": expect.any(Function), + "promptForIdenticalNamedMembers": expect.any(Function) } ); expect(response).toBe(defaultReturn); @@ -162,7 +164,8 @@ describe("DsHandler", () => { "replace": commandParameters.arguments.replace, "responseTimeout": commandParameters.arguments.responseTimeout, "safeReplace": commandParameters.arguments.safeReplace, - "promptFn": expect.any(Function) + "promptFn": expect.any(Function), + "promptForIdenticalNamedMembers": expect.any(Function) } ); expect(response).toBe(defaultReturn); @@ -198,7 +201,7 @@ describe("DsHandler", () => { const result = await promptFn(commandParameters.arguments.toDataSetName); expect(promptMock).toHaveBeenCalledWith( - `The dataset '${toDataSetName}' exists on the target system. This copy will result in data loss.` + + `The dataset '${toDataSetName}' exists on the target system. This copy can result in data loss.` + ` Are you sure you want to continue? [y/N]: ` ); expect(result).toBe(true); @@ -234,7 +237,79 @@ describe("DsHandler", () => { const result = await promptFn(commandParameters.arguments.toDataSetName); expect(promptMock).toHaveBeenCalledWith( - `The dataset '${toDataSetName}' exists on the target system. This copy will result in data loss.` + + `The dataset '${toDataSetName}' exists on the target system. This copy can result in data loss.` + + ` Are you sure you want to continue? [y/N]: ` + ); + expect(result).toBe(false); + }); + it("should prompt the user about duplicate member names and return true when input is 'y", async () => { + const handler = new DsHandler(); + + expect(handler).toBeInstanceOf(ZosFilesBaseHandler); + const fromDataSetName = "ABCD"; + const toDataSetName = "EFGH"; + const enq = "SHR"; + const replace = false; + const safeReplace = false; + const responseTimeout: any = undefined; + + const commandParameters: any = { + arguments: { + fromDataSetName, + toDataSetName, + enq, + replace, + safeReplace, + responseTimeout + }, + response: { + console: { promptFn: jest.fn() } + } + }; + const promptMock = jest.fn(); + promptMock.mockResolvedValue("y"); + + const promptForDuplicates = (handler as any)["promptForIdenticalNamedMembers"]({ prompt: promptMock }); + const result = await promptForDuplicates(); + + expect(promptMock).toHaveBeenCalledWith( + `The source and target data sets have identical member names. The contents of the target members will be overwritten.` + + ` Are you sure you want to continue? [y/N]: ` + ); + expect(result).toBe(true); + }); + it("should prompt the user about duplicate member names and return false when input is 'N'", async () => { + const handler = new DsHandler(); + + expect(handler).toBeInstanceOf(ZosFilesBaseHandler); + const fromDataSetName = "ABCD"; + const toDataSetName = "EFGH"; + const enq = "SHR"; + const replace = false; + const safeReplace = false; + const responseTimeout: any = undefined; + + const commandParameters: any = { + arguments: { + fromDataSetName, + toDataSetName, + enq, + replace, + safeReplace, + responseTimeout + }, + response: { + console: { promptFn: jest.fn() } + } + }; + const promptMock = jest.fn(); + promptMock.mockResolvedValue("N"); + + const promptForDuplicates = (handler as any)["promptForIdenticalNamedMembers"]({ prompt: promptMock }); + const result = await promptForDuplicates(); + + expect(promptMock).toHaveBeenCalledWith( + `The source and target data sets have identical member names. The contents of the target members will be overwritten.` + ` Are you sure you want to continue? [y/N]: ` ); expect(result).toBe(false); diff --git a/packages/cli/__tests__/zosfiles/__unit__/copy/ds/__snapshots__/Ds.definition.unit.test.ts.snap b/packages/cli/__tests__/zosfiles/__unit__/copy/ds/__snapshots__/Ds.definition.unit.test.ts.snap index cb2c3439b..cae8422c3 100644 --- a/packages/cli/__tests__/zosfiles/__unit__/copy/ds/__snapshots__/Ds.definition.unit.test.ts.snap +++ b/packages/cli/__tests__/zosfiles/__unit__/copy/ds/__snapshots__/Ds.definition.unit.test.ts.snap @@ -6,7 +6,7 @@ Array [ "aliases": Array [ "rep", ], - "description": "Specify this option as true if you wish to replace like-named members in the target data set", + "description": "Specify this option as true if you wish to replace members with identical names in the target data set", "name": "replace", "type": "boolean", }, @@ -15,7 +15,7 @@ Array [ "safe-rep", "sr", ], - "description": "Specify this option as true if you wish to replace like-named members or the content of the target data set. This option will prompt to confirm.", + "description": "Specify this option as true if you wish to replace members with identical names or the content of the target data set. This option will prompt to confirm.", "name": "safe-replace", "type": "boolean", }, @@ -41,7 +41,7 @@ Array [ "options": "\\"USER.FROM.SET(mem1)\\" \\"USER.TO.SET\\"", }, Object { - "description": "Copy the data set named 'USER.FROM.SET' to the data set named 'USER.TO.SET' and replace like-named members", + "description": "Copy the data set named 'USER.FROM.SET' to the data set named 'USER.TO.SET' and replace members with identical names", "options": "\\"USER.FROM.SET\\" \\"USER.TO.SET\\" --replace", }, Object { diff --git a/packages/cli/src/zosfiles/-strings-/en.ts b/packages/cli/src/zosfiles/-strings-/en.ts index 763bb166e..5697f4487 100644 --- a/packages/cli/src/zosfiles/-strings-/en.ts +++ b/packages/cli/src/zosfiles/-strings-/en.ts @@ -196,16 +196,16 @@ export default { TODSNAME: "The name of the data set that you want to copy to" }, OPTIONS: { - REPLACE: "Specify this option as true if you wish to replace like-named members in the target data set", - SAFE_REPLACE: "Specify this option as true if you wish to replace like-named members or the content of the target data set. " + - "This option will prompt to confirm." + REPLACE: "Specify this option as true if you wish to replace members with identical names in the target data set", + SAFE_REPLACE: "Specify this option as true if you wish to replace members with identical names or the " + + "content of the target data set. This option will prompt to confirm." }, EXAMPLES: { EX1: "Copy the data set named 'USER.FROM.SET' to the data set named 'USER.TO.SET'", EX2: "Copy the data set member named 'USER.FROM.SET(MEM1)' to the data set member named 'USER.TO.SET(MEM2)'", EX3: "Copy the data set named 'USER.FROM.SET' to the data set member named 'USER.TO.SET(MEM2)'", EX4: "Copy the data set member named 'USER.FROM.SET(MEM1)' to the data set named 'USER.TO.SET'", - EX5: "Copy the data set named 'USER.FROM.SET' to the data set named 'USER.TO.SET' and replace like-named members", + EX5: "Copy the data set named 'USER.FROM.SET' to the data set named 'USER.TO.SET' and replace members with identical names", EX6: "Copy the partitioned data set named 'TEST.PDS1' to the partitioned data set named 'TEST.PDS2'", EX7: "Copy the partitioned data set named 'EXISTING.PDS' to a non-existent target 'NEW.PDS'" } diff --git a/packages/cli/src/zosfiles/copy/ds/Ds.handler.ts b/packages/cli/src/zosfiles/copy/ds/Ds.handler.ts index 40913a7d5..0d899fec8 100644 --- a/packages/cli/src/zosfiles/copy/ds/Ds.handler.ts +++ b/packages/cli/src/zosfiles/copy/ds/Ds.handler.ts @@ -26,7 +26,8 @@ export default class DsHandler extends ZosFilesBaseHandler { replace: commandParameters.arguments.replace, responseTimeout: commandParameters.arguments.responseTimeout, safeReplace: commandParameters.arguments.safeReplace, - promptFn: this.promptForSafeReplace(commandParameters.response.console) + promptFn: this.promptForSafeReplace(commandParameters.response.console), + promptForIdenticalNamedMembers: this.promptForIdenticalNamedMembers(commandParameters.response.console) }; return Copy.dataSet(session, toDataSet, options); @@ -35,7 +36,17 @@ export default class DsHandler extends ZosFilesBaseHandler { private promptForSafeReplace(console: IHandlerResponseConsoleApi) { return async (targetDSN: string) => { const answer: string = await console.prompt( - `The dataset '${targetDSN}' exists on the target system. This copy will result in data loss.` + + `The dataset '${targetDSN}' exists on the target system. This copy can result in data loss.` + + ` Are you sure you want to continue? [y/N]: ` + ); + return answer != null && (answer.toLowerCase() === "y" || answer.toLowerCase() === "yes"); + }; + } + + private promptForIdenticalNamedMembers(console: IHandlerResponseConsoleApi) { + return async() => { + const answer: string = await console.prompt ( + `The source and target data sets have identical member names. The contents of the target members will be overwritten.` + ` Are you sure you want to continue? [y/N]: ` ); return answer != null && (answer.toLowerCase() === "y" || answer.toLowerCase() === "yes"); diff --git a/packages/zosfiles/CHANGELOG.md b/packages/zosfiles/CHANGELOG.md index 8dc480e13..c410662b0 100644 --- a/packages/zosfiles/CHANGELOG.md +++ b/packages/zosfiles/CHANGELOG.md @@ -2,14 +2,15 @@ All notable changes to the Zowe z/OS files SDK package will be documented in this file. +## Recent Changes +- BugFix: Users were not warned when copying partitioned data sets with identical member names. Now, the user is prompted to confirm before continuing the copy operation to avoid potential data loss. [#2349] (https://github.com/zowe/zowe-cli/issues/2349) + ## `8.13.0` - BugFix: The `Create.dataSetValidateOptions()` function now correctly handles data set creation when the `dsorg` attribute is set to `PS-L` by automatically updating the `dsntype` attribute to `LARGE`. [#2141](https://github.com/zowe/zowe-cli/issues/2141) - BugFix: Fixed an issue in the `Copy.dataSetCrossLPAR()` function where the `spacu` attribute of the copied data set was always set to `TRK`, regardless of the source data set's attributes. [#2412](https://github.com/zowe/zowe-cli/issues/2412) - BugFix: The `Copy.data.set` function now prompts the user to confirm before overwriting the contents of the target data set with the addition of the `--safe-replace` option. [#2369] (https://github.com/zowe/zowe-cli/issues/2369) -## `8.12.0` - ## `8.12.0` - Enhancement: The `Copy.dataset` function now creates a new data set if the entered target data set does not exist. [#2349](https://github.com/zowe/zowe-cli/issues/2349) - Enhancement: Added the `maxLength` option to List SDK functions (`allMembers`, `dataSetsMatchingPattern`, `membersMatchingPattern`) to specify the maximum number of items to return. [#2409](https://github.com/zowe/zowe-cli/pull/2409) diff --git a/packages/zosfiles/__tests__/__system__/methods/copy/Copy.system.test.ts b/packages/zosfiles/__tests__/__system__/methods/copy/Copy.system.test.ts index 091015314..780b99c38 100644 --- a/packages/zosfiles/__tests__/__system__/methods/copy/Copy.system.test.ts +++ b/packages/zosfiles/__tests__/__system__/methods/copy/Copy.system.test.ts @@ -19,12 +19,12 @@ import { ITestPropertiesSchema } from "../../../../../../__tests__/__src__/prope import { join } from "path"; import { readFileSync } from "fs"; import { ITestEnvironment } from "../../../../../../__tests__/__src__/environment/ITestEnvironment"; -import { tmpdir } from "os"; import path = require("path"); import * as fs from "fs"; +import * as util from "util"; +import { tmpdir } from "os"; import { ZosmfRestClient } from "@zowe/core-for-zowe-sdk"; - let REAL_SESSION: Session; let REAL_TARGET_SESSION: Session; let testEnvironment: ITestEnvironment; @@ -65,6 +65,14 @@ describe("Copy", () => { } }); describe("Success Scenarios", () => { + afterEach(async () => { + try { + await Delete.dataSet(REAL_SESSION, fromDataSetName); + await Delete.dataSet(REAL_SESSION, toDataSetName); + } catch (err) { + Imperative.console.info(`Error: ${inspect(err)}`); + } + }); describe("Sequential > Sequential", () => { beforeEach(async () => { try { @@ -106,7 +114,7 @@ describe("Copy", () => { expect(contents1.toString()).toEqual(contents2.toString()); }); }); - describe("Partioned > Partioned", () => { + describe("Partitioned > Partitioned", () => { let downloadDir: string; beforeEach(async () => { try { @@ -127,8 +135,6 @@ describe("Copy", () => { it("Should copy a partitioned data set", async () => { let error; let response; - let contents1; - let contents2; try { response = await Copy.dataSet( @@ -138,8 +144,6 @@ describe("Copy", () => { dsn:fromDataSetName }} ); - contents1 = await Get.dataSet(REAL_SESSION, fromDataSetName); - contents2 = await Get.dataSet(REAL_SESSION, toDataSetName); Imperative.console.info(`Response: ${inspect(response)}`); } catch (err) { error = err; @@ -151,13 +155,42 @@ describe("Copy", () => { expect(response).toBeTruthy(); expect(response.success).toBe(true); expect(response.commandResponse).toContain(ZosFilesMessages.datasetCopiedSuccessfully.message); - - expect(contents1).toBeTruthy(); - expect(contents2).toBeTruthy(); - expect(contents1.toString()).toEqual(contents2.toString()); }); - afterEach(() => { - fs.rmSync(downloadDir, { recursive: true, force: true }); + it("Should handle truncation errors and log them to a file", async () => { + let error; + let response; + + const uploadFileToDatasetSpy = jest.spyOn(Upload, 'fileToDataset').mockImplementation(async (session, filePath, dsn) => { + if (filePath === fileLocation) { + throw new Error("Truncation of a record occurred during an I/O operation"); + } + return Promise.resolve() as any; + }); + const copyDataSetSpy = jest.spyOn(Copy, 'dataSet').mockImplementation(async () => { + return { + success: true, + commandResponse: ZosFilesMessages.datasetCopiedSuccessfully.message + " " + + util.format(ZosFilesMessages.membersContentTruncated.message) + }; + }); + try { + response = await Copy.dataSet( + REAL_SESSION, + {dsn: toDataSetName}, + {"from-dataset": { + dsn:fromDataSetName + }} + ); + } catch (err) { + error = err; + Imperative.console.info(`Error: ${inspect(err)}`); + } + expect(response).toBeTruthy(); + expect(response.success).toBe(true); + expect(response.commandResponse).toContain(ZosFilesMessages.datasetCopiedSuccessfully.message + " " + + util.format(ZosFilesMessages.membersContentTruncated.message)); + uploadFileToDatasetSpy.mockRestore(); + copyDataSetSpy.mockRestore(); }); }); describe("Member > Member", () => { @@ -597,6 +630,45 @@ describe("Copy", () => { }); }); + describe("hasIdenticalMemberNames", () => { + beforeEach(async () => { + try { + await Create.dataSet(REAL_SESSION, CreateDataSetTypeEnum.DATA_SET_PARTITIONED, fromDataSetName); + await Create.dataSet(REAL_SESSION, CreateDataSetTypeEnum.DATA_SET_PARTITIONED, toDataSetName); + await Upload.fileToDataset(REAL_SESSION, fileLocation, fromDataSetName); + await Upload.fileToDataset(REAL_SESSION, fileLocation, toDataSetName); + } + catch (err) { + Imperative.console.info(`Error: ${inspect(err)}`); + } + }); + afterEach(async () => { + try { + await Delete.dataSet(REAL_SESSION, fromDataSetName); + await Delete.dataSet(REAL_SESSION, toDataSetName); + } catch (err) { + Imperative.console.info(`Error: ${inspect(err)}`); + } + }); + it("should return true if the source and target data sets have identical member names", async () => { + const sourceResponse = await List.allMembers(REAL_SESSION, fromDataSetName); + const sourceMemberList = sourceResponse.apiResponse.items.map((item: { member: any; }) => item.member); + const response = await Copy["hasIdenticalMemberNames"](REAL_SESSION, sourceMemberList, toDataSetName); + expect(response).toBe(true); + }); + + it("should return false if the source and target data sets do not have identical member names", async () => { + await Delete.dataSet(REAL_SESSION, toDataSetName); + await Create.dataSet(REAL_SESSION, CreateDataSetTypeEnum.DATA_SET_PARTITIONED, toDataSetName); + + const sourceResponse = await List.allMembers(REAL_SESSION, fromDataSetName); + const sourceMemberList = sourceResponse.apiResponse.items.map((item: { member: any; }) => item.member); + + const response = await Copy["hasIdenticalMemberNames"](REAL_SESSION, sourceMemberList, toDataSetName); + expect(response).toBe(false); + }); + }); + describe("Data Set Cross LPAR", () => { describe("Common Failures", () => { it("should fail if no fromDataSet data set name is supplied", async () => { diff --git a/packages/zosfiles/__tests__/__unit__/methods/copy/Copy.unit.test.ts b/packages/zosfiles/__tests__/__unit__/methods/copy/Copy.unit.test.ts index 4ad0f10ef..43ac24463 100644 --- a/packages/zosfiles/__tests__/__unit__/methods/copy/Copy.unit.test.ts +++ b/packages/zosfiles/__tests__/__unit__/methods/copy/Copy.unit.test.ts @@ -35,15 +35,19 @@ describe("Copy", () => { const toDataSetName = "USER.DATA.TO"; const toMemberName = "mem2"; const isPDSSpy = jest.spyOn(Copy as any, "isPDS"); + const hasIdenticalMemberNames = jest.spyOn(Copy as any, "hasIdenticalMemberNames"); let dataSetExistsSpy: jest.SpyInstance; const promptFn = jest.fn(); + const promptForIdenticalNamedMembers = jest.fn(); + const listAllMembersSpy = jest.spyOn(List, "allMembers"); beforeEach(() => { copyPDSSpy.mockClear(); copyExpectStringSpy.mockClear().mockImplementation(async () => { return ""; }); isPDSSpy.mockClear().mockResolvedValue(false); dataSetExistsSpy = jest.spyOn(Copy as any, "dataSetExists").mockResolvedValue(true); - + hasIdenticalMemberNames.mockClear().mockResolvedValue(false); + promptForIdenticalNamedMembers.mockClear(); }); afterAll(() => { isPDSSpy.mockRestore(); @@ -512,6 +516,14 @@ describe("Copy", () => { describe("Partitioned > Partitioned", () => { let createSpy: jest.SpyInstance; let dataSetExistsSpy: jest.SpyInstance; + const sourceResponse = { + apiResponse: { + items: [ + { member: "mem1" }, + { member: "mem2" }, + ] + } + }; beforeEach(() => { isPDSSpy.mockClear().mockResolvedValue(true); copyPDSSpy.mockClear().mockResolvedValue({success: true, commandResponse: ZosFilesMessages.datasetCopiedSuccessfully.message}); @@ -520,6 +532,7 @@ describe("Copy", () => { commandResponse: ZosFilesMessages.dataSetCreatedSuccessfully.message }); dataSetExistsSpy = jest.spyOn(Copy as any, "dataSetExists"); + listAllMembersSpy.mockImplementation(async (): Promise => sourceResponse); }); afterAll(() => { copyPDSSpy.mockRestore(); @@ -540,8 +553,6 @@ describe("Copy", () => { expect(isPDSSpy).toHaveBeenNthCalledWith(2, dummySession, toDataSetName); expect(copyPDSSpy).toHaveBeenCalledTimes(1); - expect(copyPDSSpy).toHaveBeenCalledWith(dummySession, fromDataSetName, toDataSetName); - expect(response).toEqual({ success: true, commandResponse: ZosFilesMessages.datasetCopiedSuccessfully.message @@ -619,6 +630,50 @@ describe("Copy", () => { commandResponse: ZosFilesMessages.datasetCopiedSuccessfully.message }); }); + it("should display a prompt for identical member names if there are identical member names and" + + "--safe-replace and --replace flags are not used", async () => { + hasIdenticalMemberNames.mockResolvedValue(true); + promptForIdenticalNamedMembers.mockClear().mockResolvedValue(true); + + const response = await Copy.dataSet( + dummySession, + { dsn: toDataSetName }, + { "from-dataset": { dsn: fromDataSetName }, + safeReplace: false, + replace: false, + promptForIdenticalNamedMembers } + ); + expect(promptForIdenticalNamedMembers).toHaveBeenCalledWith(); + expect(response.success).toEqual(true); + + }); + it("should not display a prompt for identical member names if there are no identical member names", async () => { + const response = await Copy.dataSet( + dummySession, + { dsn: toDataSetName }, + { "from-dataset": { dsn: fromDataSetName }, + safeReplace: false, + replace: false, + promptForIdenticalNamedMembers } + ); + expect(response.success).toEqual(true); + expect(promptForIdenticalNamedMembers).not.toHaveBeenCalled(); + }); + it("should throw error if user declines to replace the dataset", async () => { + hasIdenticalMemberNames.mockResolvedValue(true); + promptForIdenticalNamedMembers.mockClear().mockResolvedValue(false); + + await expect(Copy.dataSet( + dummySession, + { dsn: toDataSetName }, + { "from-dataset": { dsn: fromDataSetName }, + safeReplace: false, + replace: false, + promptForIdenticalNamedMembers } + )).rejects.toThrow(new ImperativeError({ msg: ZosFilesMessages.datasetCopiedAborted.message })); + + expect(promptForIdenticalNamedMembers).toHaveBeenCalled(); + }); }); it("should return early if the source and target data sets are identical", async () => { const response = await Copy.dataSet( @@ -711,7 +766,7 @@ describe("Copy", () => { }); }); - describe("Copy Partitioned Data Set", () => { + describe("Partitioned Data Set", () => { const listAllMembersSpy = jest.spyOn(List, "allMembers"); const downloadAllMembersSpy = jest.spyOn(Download, "allMembers"); const uploadSpy = jest.spyOn(Upload, "streamToDataSet"); @@ -722,6 +777,11 @@ describe("Copy", () => { const readStream = jest.spyOn(IO, "createReadStream"); const rmSync = jest.spyOn(fs, "rmSync"); const listDatasetSpy = jest.spyOn(List, "dataSet"); + const hasIdenticalMemberNames = jest.spyOn(Copy as any, "hasIdenticalMemberNames"); + + beforeEach(() => { + hasIdenticalMemberNames.mockRestore(); + }); const dsPO = { dsname: fromDataSetName, @@ -818,6 +878,7 @@ describe("Copy", () => { ] } }; + const sourceMemberList = sourceResponse.apiResponse.items.map((item: { member: any; }) => item.member); const fileList = ["mem1", "mem2"]; listAllMembersSpy.mockImplementation(async (): Promise => sourceResponse); downloadAllMembersSpy.mockImplementation(async (): Promise => undefined); @@ -829,7 +890,7 @@ describe("Copy", () => { try{ - response = await Copy.copyPDS(dummySession, fromDataSetName, toDataSetName); + response = await Copy.copyPDS(dummySession, sourceMemberList, fromDataSetName, toDataSetName); } catch(e) { // Do nothing @@ -844,6 +905,71 @@ describe("Copy", () => { commandResponse: ZosFilesMessages.datasetCopiedSuccessfully.message, }); }); + + describe("hasIdenticalMemberNames", () => { + const listAllMembersSpy = jest.spyOn(List, "allMembers"); + + beforeEach(() => { + jest.clearAllMocks(); + }); + it("should return true if the source and target have identical member names", async () => { + const sourceResponse = { + apiResponse: { + items: [ + { member: "mem1" }, + { member: "mem2" }, + ] + } + }; + const targetResponse = { + apiResponse: { + items: [ + { member: "mem1" }, + ] + } + }; + listAllMembersSpy.mockImplementation(async (session, dsName): Promise => { + if (dsName === fromDataSetName) { + return sourceResponse; + } else if (dsName === toDataSetName) { + return targetResponse; + } + }); + const sourceMemberList = sourceResponse.apiResponse.items.map((item: { member: any; }) => item.member); + const response = await Copy["hasIdenticalMemberNames"](dummySession, sourceMemberList, toDataSetName); + expect(response).toBe(true); + expect(listAllMembersSpy).toHaveBeenCalledWith(dummySession, toDataSetName); + }); + it("should return false if the source and target do not have identcal member names", async () => { + const sourceResponse = { + apiResponse: { + items: [ + { member: "mem1" }, + { member: "mem2" }, + ] + } + }; + const targetResponse = { + apiResponse: { + items: [ + { member: "mem3" }, + ] + } + }; + listAllMembersSpy.mockImplementation(async (session, dsName): Promise => { + if (dsName === fromDataSetName) { + return sourceResponse; + } else if (dsName === toDataSetName) { + return targetResponse; + } + }); + const sourceMemberList = sourceResponse.apiResponse.items.map((item: { member: any; }) => item.member); + const response = await Copy["hasIdenticalMemberNames"](dummySession, sourceMemberList, toDataSetName); + + expect(response).toBe(false); + expect(listAllMembersSpy).toHaveBeenCalledWith(dummySession, toDataSetName); + }); + }); }); describe("Data Set Cross LPAR", () => { diff --git a/packages/zosfiles/__tests__/__unit__/utils/__snapshots__/ZosFilesUtils.unit.test.ts.snap b/packages/zosfiles/__tests__/__unit__/utils/__snapshots__/ZosFilesUtils.unit.test.ts.snap index 61acd4749..2b81c6b28 100644 --- a/packages/zosfiles/__tests__/__unit__/utils/__snapshots__/ZosFilesUtils.unit.test.ts.snap +++ b/packages/zosfiles/__tests__/__unit__/utils/__snapshots__/ZosFilesUtils.unit.test.ts.snap @@ -200,6 +200,9 @@ Destination: %s", "message": "Member(s) downloaded successfully. Destination: %s", }, + "membersContentTruncated": Object { + "message": "Member(s)' contents were truncated due to insufficient record length. You can view the list of members here: %s", + }, "membersMatchedPattern": Object { "message": "%d members(s) were found matching pattern.", }, diff --git a/packages/zosfiles/src/constants/ZosFiles.messages.ts b/packages/zosfiles/src/constants/ZosFiles.messages.ts index 9c4b9d8e7..5a3e4c30e 100644 --- a/packages/zosfiles/src/constants/ZosFiles.messages.ts +++ b/packages/zosfiles/src/constants/ZosFiles.messages.ts @@ -189,7 +189,6 @@ export const ZosFilesMessages: { [key: string]: IMessageDefinition } = { message: "Member(s) downloaded successfully." }, - /** * Message indicating that the member was downloaded successfully * @type {IMessageDefinition} @@ -198,6 +197,13 @@ export const ZosFilesMessages: { [key: string]: IMessageDefinition } = { message: "Member(s) downloaded successfully.\nDestination: %s" }, + /** + * Message indicating that the members contents were truncated due to lrecl + * @type {IMessageDefinition} + */ + membersContentTruncated: { + message: "Member(s)' contents were truncated due to insufficient record length. You can view the list of members here: %s" + }, /** * Message indicating that the uss file was downloaded successfully * @type {IMessageDefinition} diff --git a/packages/zosfiles/src/methods/copy/Copy.ts b/packages/zosfiles/src/methods/copy/Copy.ts index 2894cc5c4..1b10e8917 100644 --- a/packages/zosfiles/src/methods/copy/Copy.ts +++ b/packages/zosfiles/src/methods/copy/Copy.ts @@ -58,6 +58,7 @@ export class Copy { ImperativeExpect.toBeDefinedAndNonBlank(options["from-dataset"].dsn, "fromDataSetName"); ImperativeExpect.toBeDefinedAndNonBlank(toDataSetName, "toDataSetName"); const safeReplace: boolean = options.safeReplace; + const overwriteMembers: boolean = options.replace; if(options["from-dataset"].dsn === toDataSetName && toMemberName === options["from-dataset"].member) { return { @@ -84,8 +85,20 @@ export class Copy { if(!toMemberName && !options["from-dataset"].member) { const sourceIsPds = await this.isPDS(session, options["from-dataset"].dsn); const targetIsPds = await this.isPDS(session, toDataSetName); + if (sourceIsPds && targetIsPds) { - const response = await this.copyPDS(session, options["from-dataset"].dsn, toDataSetName); + const sourceResponse = await List.allMembers(session, options["from-dataset"].dsn); + const sourceMemberList = sourceResponse.apiResponse.items.map((item: { member: any; }) => item.member); + + const hasIdenticalMemberNames = await this.hasIdenticalMemberNames(session, sourceMemberList, toDataSetName); + if(!safeReplace && hasIdenticalMemberNames && !overwriteMembers) { + const userResponse = await options.promptForIdenticalNamedMembers(); + + if(!userResponse) { + throw new ImperativeError({ msg: ZosFilesMessages.datasetCopiedAborted.message}); + } + } + const response = await this.copyPDS(session, sourceMemberList, options["from-dataset"].dsn, toDataSetName); return { success: true, commandResponse: newDataSet @@ -170,6 +183,20 @@ export class Copy { return dsnameIndex !== -1; } + /** + * Function that checks if source and target data sets have identical member names + */ + private static async hasIdenticalMemberNames ( + session: AbstractSession, + sourceMemberList: string[], + toPds: string + ): Promise { + const targetResponse = await List.allMembers(session, toPds); + const targetMemberList = targetResponse.apiResponse.items.map((item: { member: any; }) => item.member); + + return sourceMemberList.some((mem: any) => targetMemberList.includes(mem)); + } + /** * Copy the members of a Partitioned dataset into another Partitioned dataset * @@ -186,10 +213,8 @@ export class Copy { */ public static async copyPDS ( - session: AbstractSession, fromPds: string, toPds: string): Promise { + session: AbstractSession, sourceMemberList: string[], fromPds: string, toPds: string): Promise { try { - const sourceResponse = await List.allMembers(session, fromPds); - const sourceMemberList: Array<{ member: string }> = sourceResponse.apiResponse.items; if(sourceMemberList.length == 0) { return { @@ -203,7 +228,8 @@ export class Copy { const uploadFileList: string[] = ZosFilesUtils.getFileListFromPath(downloadDir); for (const file of uploadFileList) { - const uploadingDsn = `${toPds}(${ZosFilesUtils.generateMemberName(file)})`; + const memName = ZosFilesUtils.generateMemberName(file); + const uploadingDsn = `${toPds}(${memName})`; const uploadStream = IO.createReadStream(file); await Upload.streamToDataSet(session, uploadStream, uploadingDsn); } diff --git a/packages/zosfiles/src/methods/copy/doc/ICopyDatasetOptions.ts b/packages/zosfiles/src/methods/copy/doc/ICopyDatasetOptions.ts index 23d75b796..fabcc3659 100644 --- a/packages/zosfiles/src/methods/copy/doc/ICopyDatasetOptions.ts +++ b/packages/zosfiles/src/methods/copy/doc/ICopyDatasetOptions.ts @@ -47,4 +47,10 @@ export interface ICopyDatasetOptions extends IZosFilesOptions { * @returns True if target data set should be overwritten */ promptFn?: (targetDSN: string) => Promise; + + /** + * Prompt for duplicates + * @returns True if target data set members + */ + promptForIdenticalNamedMembers?: () => Promise; }