diff --git a/src/commands/configuration-management/api/batch-import-export-api.ts b/src/commands/configuration-management/api/batch-import-export-api.ts index 619f45e..6b6b8ee 100644 --- a/src/commands/configuration-management/api/batch-import-export-api.ts +++ b/src/commands/configuration-management/api/batch-import-export-api.ts @@ -52,6 +52,17 @@ export class BatchImportExportApi { }); } + public async findPackagesByKeysAndVersion(packageKeysWithVersion: string[], withDependencies: boolean = false): Promise { + const queryParams = new URLSearchParams(); + + packageKeysWithVersion.forEach(keyWithVersion => queryParams.append("packageKeysWithVersion", keyWithVersion)); + queryParams.set("withDependencies", withDependencies.toString()); + + return this.httpClient().get(`/package-manager/api/core/packages/versions/export/list?${queryParams.toString()}`).catch(e => { + throw new FatalError(`Problem getting packages by keys and versions: ${e}`); + }); + } + public async exportPackages(packageKeys: string[], withDependencies: boolean): Promise { const queryParams = new URLSearchParams(); packageKeys.forEach(packageKey => queryParams.append("packageKeys", packageKey)); diff --git a/src/commands/configuration-management/batch-import-export.service.ts b/src/commands/configuration-management/batch-import-export.service.ts index dd4c865..8b45b09 100644 --- a/src/commands/configuration-management/batch-import-export.service.ts +++ b/src/commands/configuration-management/batch-import-export.service.ts @@ -41,12 +41,14 @@ export class BatchImportExportService { }); } - public async findAndExportListOfActivePackages(flavors: string[], packageKeys: string[], withDependencies: boolean): Promise { + public async findAndExportListOfPackages(flavors: string[], packageKeys: string[], keysByVersion: string[], withDependencies: boolean): Promise { let packagesToExport: PackageExportTransport[]; - if (packageKeys.length) { + if (keysByVersion.length) { + packagesToExport = await this.batchImportExportApi.findPackagesByKeysAndVersion(keysByVersion, withDependencies); + } else if (packageKeys.length) { packagesToExport = await this.batchImportExportApi.findActivePackagesByKeys(packageKeys, withDependencies); - } else { + } else { packagesToExport = await this.batchImportExportApi.findAllActivePackages(flavors, withDependencies); } @@ -55,6 +57,13 @@ export class BatchImportExportService { this.exportListOfPackages(packagesToExport); } + public async listPackagesByKeysWithVersion(keysByVersion: string[], withDependencies: boolean): Promise { + const exportedPackages = await this.batchImportExportApi.findPackagesByKeysAndVersion(keysByVersion, withDependencies); + exportedPackages.forEach(pkg => { + logger.info(`${pkg.name} - Key: "${pkg.key}"`); + }); + } + public async batchExportPackages(packageKeys: string[], packageKeysByVersion: string[], withDependencies: boolean, gitBranch: string, unzip: boolean): Promise { let exportedPackagesData: Buffer; if (packageKeys) { diff --git a/src/commands/configuration-management/config-command.service.ts b/src/commands/configuration-management/config-command.service.ts index 6e0ed49..b0a903f 100644 --- a/src/commands/configuration-management/config-command.service.ts +++ b/src/commands/configuration-management/config-command.service.ts @@ -16,14 +16,16 @@ export class ConfigCommandService { this.diffService = new DiffService(context); } - public async listActivePackages(jsonResponse: boolean, flavors: string[], withDependencies: boolean, packageKeys: string[], variableValue: string, variableType: string): Promise { + public async listPackages(jsonResponse: boolean, flavors: string[], withDependencies: boolean, packageKeys: string[], keysByVersion: string[], variableValue: string, variableType: string): Promise { if (variableValue) { await this.listPackagesByVariableValue(jsonResponse, flavors, variableValue, variableType); return; } if (jsonResponse) { - await this.batchImportExportService.findAndExportListOfActivePackages(flavors ?? [], packageKeys ?? [], withDependencies) + await this.batchImportExportService.findAndExportListOfPackages(flavors ?? [], packageKeys ?? [], keysByVersion ?? [], withDependencies); + } else if (keysByVersion) { + await this.batchImportExportService.listPackagesByKeysWithVersion(keysByVersion, withDependencies); } else { await this.batchImportExportService.listActivePackages(flavors ?? []); } diff --git a/src/commands/configuration-management/module.ts b/src/commands/configuration-management/module.ts index b5e3fb6..1d45f7f 100644 --- a/src/commands/configuration-management/module.ts +++ b/src/commands/configuration-management/module.ts @@ -17,14 +17,15 @@ class Module extends IModule { public register(context: Context, configurator: Configurator): void { const configCommand = configurator.command("config"); configCommand.command("list") - .description("Command to list active packages that can be exported") + .description("Command to list packages") .option("--json", "Return response as json type", "") .option("--flavors ", "Lists only active packages of the given flavors") .option("--withDependencies", "Include dependencies", "") - .option("--packageKeys ", "Lists only given package keys") + .option("--packageKeys ", "Lists only active versions of given package keys") + .option("--keysByVersion ", "Lists packages by given key and version [packageKey.version]") .option("--variableValue ", "Variable value for filtering packages by.") .option("--variableType ", "Variable type for filtering packages by.") - .action(this.listActivePackages); + .action(this.listPackages); configCommand.command("export") .description("Command to export package configs") @@ -133,8 +134,11 @@ class Module extends IModule { .action(this.listAssignments); } - private async listActivePackages(context: Context, command: Command, options: OptionValues): Promise { - await new ConfigCommandService(context).listActivePackages(options.json, options.flavors, options.withDependencies, options.packageKeys, options.variableValue, options.variableType); + private async listPackages(context: Context, command: Command, options: OptionValues): Promise { + if (options.packageKeys && options.keysByVersion) { + throw new Error("Please provide either --packageKeys or --keysByVersion, but not both."); + } + await new ConfigCommandService(context).listPackages(options.json, options.flavors, options.withDependencies, options.packageKeys, options.keysByVersion, options.variableValue, options.variableType); } private async batchExportPackages(context: Context, command: Command, options: OptionValues): Promise { diff --git a/tests/commands/configuration-management/config-list.spec.ts b/tests/commands/configuration-management/config-list.spec.ts index b1c6af0..11898b0 100644 --- a/tests/commands/configuration-management/config-list.spec.ts +++ b/tests/commands/configuration-management/config-list.spec.ts @@ -1,5 +1,4 @@ import * as path from "path"; -import { stringify } from "../../../src/core/utils/json"; import { PacmanApiUtils } from "../../utls/pacman-api.utils"; import { mockAxiosGet } from "../../utls/http-requests-mock"; import { ConfigCommandService } from "../../../src/commands/configuration-management/config-command.service"; @@ -33,7 +32,7 @@ describe("Config list", () => { mockAxiosGet("https://myTeam.celonis.cloud/package-manager/api/core/packages/export/list?" + urlParams.toString(), [firstPackage, secondPackage]); - await new ConfigCommandService(testContext).listActivePackages(false, flavorsArray, false, [], null, null); + await new ConfigCommandService(testContext).listPackages(false, flavorsArray, false, [], undefined, null, null); expect(loggingTestTransport.logMessages.length).toBe(2); expect(loggingTestTransport.logMessages[0].message).toContain(`${firstPackage.name} - Key: "${firstPackage.key}"`); @@ -50,7 +49,7 @@ describe("Config list", () => { mockAxiosGet("https://myTeam.celonis.cloud/package-manager/api/core/packages/export/list?withDependencies=false", [{...firstPackage}, {...secondPackage}]); mockAxiosGet("https://myTeam.celonis.cloud/package-manager/api/packages/with-variable-assignments?type=DATA_MODEL", [studioPackage]); - await new ConfigCommandService(testContext).listActivePackages(true, [], false, [], null, null); + await new ConfigCommandService(testContext).listPackages(true, [], false, [], undefined, null, null); const expectedFileName = loggingTestTransport.logMessages[0].message.split(FileService.fileDownloadedMessage)[1]; @@ -96,7 +95,7 @@ describe("Config list", () => { }; mockAxiosGet("https://myTeam.celonis.cloud/package-manager/api/compute-pools/data-models/details", [dataModelDetailResponse]); - await new ConfigCommandService(testContext).listActivePackages(true, [], true, [], null, null); + await new ConfigCommandService(testContext).listPackages(true, [], true, [], undefined, null, null); const expectedFileName = loggingTestTransport.logMessages[0].message.split(FileService.fileDownloadedMessage)[1]; @@ -121,7 +120,7 @@ describe("Config list", () => { mockAxiosGet("https://myTeam.celonis.cloud/package-manager/api/core/packages/export/list-by-keys?packageKeys=key-1&packageKeys=key-2&withDependencies=false", [{...firstPackage}, {...secondPackage}]); mockAxiosGet("https://myTeam.celonis.cloud/package-manager/api/packages/with-variable-assignments?type=DATA_MODEL", [studioPackage]); - await new ConfigCommandService(testContext).listActivePackages(true, [], false, [firstPackage.key, secondPackage.key], null, null); + await new ConfigCommandService(testContext).listPackages(true, [], false, [firstPackage.key, secondPackage.key], undefined, null, null); const expectedFileName = loggingTestTransport.logMessages[0].message.split(FileService.fileDownloadedMessage)[1]; @@ -167,7 +166,7 @@ describe("Config list", () => { }; mockAxiosGet("https://myTeam.celonis.cloud/package-manager/api/compute-pools/data-models/details", [dataModelDetailResponse]); - await new ConfigCommandService(testContext).listActivePackages(true, [], true, [firstPackage.key, secondPackage.key], null, null); + await new ConfigCommandService(testContext).listPackages(true, [], true, [firstPackage.key, secondPackage.key], undefined, null, null); const expectedFileName = loggingTestTransport.logMessages[0].message.split(FileService.fileDownloadedMessage)[1]; @@ -191,7 +190,7 @@ describe("Config list", () => { mockAxiosGet("https://myTeam.celonis.cloud/package-manager/api/core/packages/export/list-by-variable-value?variableValue=1", [{...firstPackage}, {...secondPackage}]); mockAxiosGet("https://myTeam.celonis.cloud/package-manager/api/packages/with-variable-assignments?type=DATA_MODEL", []); - await new ConfigCommandService(testContext).listActivePackages(false, [], false, [], "1", null); + await new ConfigCommandService(testContext).listPackages(false, [], false, [], undefined, "1", null); expect(loggingTestTransport.logMessages.length).toBe(2); expect(loggingTestTransport.logMessages[0].message).toContain(`${firstPackage.name} - Key: "${firstPackage.key}"`); @@ -205,7 +204,7 @@ describe("Config list", () => { mockAxiosGet("https://myTeam.celonis.cloud/package-manager/api/core/packages/export/list-by-variable-value?variableValue=1", [{...firstPackage}, {...secondPackage}]); mockAxiosGet("https://myTeam.celonis.cloud/package-manager/api/packages/with-variable-assignments?type=DATA_MODEL", []); - await new ConfigCommandService(testContext).listActivePackages(true, [], false, [], "1", null); + await new ConfigCommandService(testContext).listPackages(true, [], false, [], undefined, "1", null); const expectedFileName = loggingTestTransport.logMessages[0].message.split(FileService.fileDownloadedMessage)[1]; @@ -215,4 +214,64 @@ describe("Config list", () => { expect(exportedTransports.length).toBe(2); }) + it("Should list packages by keysByVersion for non-json response", async () => { + const firstPackage = PacmanApiUtils.buildPackageExportTransport("key-1", "name-1"); + const secondPackage = PacmanApiUtils.buildPackageExportTransport("key-2", "name-2"); + + const keysByVersion = ["key-1.1.0.0", "key-2.1.0.1"]; + mockAxiosGet( + "https://myTeam.celonis.cloud/package-manager/api/core/packages/versions/export/list?packageKeysWithVersion=key-1.1.0.0&packageKeysWithVersion=key-2.1.0.1&withDependencies=false", + [firstPackage, secondPackage] + ); + + await new ConfigCommandService(testContext).listPackages(false, [], false, undefined, keysByVersion, null, null); + + expect(loggingTestTransport.logMessages.length).toBe(2); + expect(loggingTestTransport.logMessages[0].message).toContain(`${firstPackage.name} - Key: "${firstPackage.key}"`); + expect(loggingTestTransport.logMessages[1].message).toContain(`${secondPackage.name} - Key: "${secondPackage.key}"`); + }) + + it("Should list packages by keysByVersion with withDependencies for non-json response", async () => { + const firstPackage = PacmanApiUtils.buildPackageExportTransport("key-1", "name-1"); + const secondPackage = PacmanApiUtils.buildPackageExportTransport("key-2", "name-2"); + + const keysByVersion = ["key-1.1.0.0", "key-2.1.0.1"]; + mockAxiosGet( + "https://myTeam.celonis.cloud/package-manager/api/core/packages/versions/export/list?packageKeysWithVersion=key-1.1.0.0&packageKeysWithVersion=key-2.1.0.1&withDependencies=true", + [firstPackage, secondPackage] + ); + + await new ConfigCommandService(testContext).listPackages(false, [], true, undefined, keysByVersion, null, null); + + expect(loggingTestTransport.logMessages.length).toBe(2); + }) + + it("Should list packages by keysByVersion for json response", async () => { + const firstPackage = PacmanApiUtils.buildPackageExportTransport("key-1", "name-1"); + const secondPackage = PacmanApiUtils.buildPackageExportTransport("key-2", "name-2"); + + const studioPackage: ContentNodeTransport = PacmanApiUtils.buildContentNodeTransport("key-1", "spaceId-1"); + const keysByVersion = ["key-1.1.0.0", "key-2.1.0.1"]; + + mockAxiosGet( + "https://myTeam.celonis.cloud/package-manager/api/core/packages/versions/export/list?packageKeysWithVersion=key-1.1.0.0&packageKeysWithVersion=key-2.1.0.1&withDependencies=false", + [{...firstPackage}, {...secondPackage}] + ); + mockAxiosGet("https://myTeam.celonis.cloud/package-manager/api/packages/with-variable-assignments?type=DATA_MODEL", [studioPackage]); + + await new ConfigCommandService(testContext).listPackages(true, [], false, [], keysByVersion, null, null); + + const expectedFileName = loggingTestTransport.logMessages[0].message.split(FileService.fileDownloadedMessage)[1]; + + expect(mockWriteFileSync).toHaveBeenCalledWith(path.resolve(process.cwd(), expectedFileName), expect.any(String), {encoding: "utf-8"}); + + const exportedTransports = JSON.parse(mockWriteFileSync.mock.calls[0][1]) as PackageExportTransport[]; + expect(exportedTransports.length).toBe(2); + + const exportedFirstPackage = exportedTransports.filter(transport => transport.key === firstPackage.key)[0]; + const exportedSecondPackage = exportedTransports.filter(transport => transport.key === secondPackage.key)[0]; + + expect(exportedSecondPackage).toEqual(secondPackage); + expect(exportedFirstPackage).toEqual({...firstPackage, spaceId: "spaceId-1"}); + }) }) \ No newline at end of file diff --git a/tests/commands/configuration-management/module.spec.ts b/tests/commands/configuration-management/module.spec.ts index 17e62cc..c6fb499 100644 --- a/tests/commands/configuration-management/module.spec.ts +++ b/tests/commands/configuration-management/module.spec.ts @@ -19,6 +19,7 @@ describe("Configuration Management Module - Action Validations", () => { mockCommand = {} as Command; mockConfigCommandService = { + listPackages: jest.fn().mockResolvedValue(undefined), batchExportPackages: jest.fn().mockResolvedValue(undefined), batchImportPackages: jest.fn().mockResolvedValue(undefined), } as any; @@ -31,6 +32,61 @@ describe("Configuration Management Module - Action Validations", () => { (NodeDependencyService as jest.MockedClass).mockImplementation(() => mockNodeDependencyService); }); + describe("listActivePackages validation", () => { + describe("packageKeys and keysByVersion validation", () => { + it("should throw error when both packageKeys and keysByVersion are provided", async () => { + const options: OptionValues = { + packageKeys: ["package1", "package2"], + keysByVersion: ["package3.1.0.0", "package4.1.0.0"], + }; + + await expect( + (module as any).listPackages(testContext, mockCommand, options) + ).rejects.toThrow("Please provide either --packageKeys or --keysByVersion, but not both."); + + expect(mockConfigCommandService.listPackages).not.toHaveBeenCalled(); + }); + + it("should pass validation when only packageKeys is provided", async () => { + const options: OptionValues = { + packageKeys: ["package1", "package2"], + json: true, + }; + + await (module as any).listPackages(testContext, mockCommand, options); + + expect(mockConfigCommandService.listPackages).toHaveBeenCalledWith( + true, + undefined, + undefined, + ["package1", "package2"], + undefined, + undefined, + undefined + ); + }); + + it("should pass validation when only keysByVersion is provided", async () => { + const options: OptionValues = { + keysByVersion: ["package3.1.0.0", "package4.1.0.0"], + json: true, + }; + + await (module as any).listPackages(testContext, mockCommand, options); + + expect(mockConfigCommandService.listPackages).toHaveBeenCalledWith( + true, + undefined, + undefined, + undefined, + ["package3.1.0.0", "package4.1.0.0"], + undefined, + undefined + ); + }); + }); + }); + describe("batchExportPackages validation", () => { describe("packageKeys and keysByVersion validation", () => { it("should throw error when both packageKeys and keysByVersion are provided", async () => {