diff --git a/docs/user-guide/config-commands.md b/docs/user-guide/config-commands.md index 922bdb7..d830886 100644 --- a/docs/user-guide/config-commands.md +++ b/docs/user-guide/config-commands.md @@ -147,13 +147,17 @@ info: Config import report file: 9560f81f-f746-4117-83ee-dd1f614ad624.json ### Listing Package Variables -Package variables (with assignments) can be listed with the following command: +Variables can be read for **published package versions** (each package identified with a version) or for the **unpublished** configuration of packages (identified by package key only). Use either the published flow (`--keysByVersion` / `--keysByVersionFile`) or the unpublished flow (`--packageKeys`); combining them is not supported and the command will fail. + +**Output (console and `--json`).** For both flows, each package is represented the same way: `packageKey`, `variables` (definitions and values), andβ€”**only for published versions**β€”`version`. Without `--json`, each package is printed as one JSON object per line. With `--json`, the result is written to a file as a **JSON array** of those objects. For unpublished packages, `version` is simply omitted. + +**Published versions** β€” `config variables list` with `--keysByVersion` or `--keysByVersionFile`: ```bash content-cli config variables list -p --keysByVersion key1:version1 ... keyN:versionN ``` -The --keysByVersion option should specify a list of key :(colon) version pairs. Alternatively, a json file path containing a list of key and version pairs can be used. The PackageKeyAndVersionPair for the file should have the following form: +The `--keysByVersion` option should specify a list of `key:version` pairs. Alternatively, pass a JSON file path with `--keysByVersionFile`. Each entry in the file should match: ```typescript export interface PackageKeyAndVersionPair { @@ -162,7 +166,13 @@ export interface PackageKeyAndVersionPair { } ``` -Similar to the other listing commands, the --json option can be used for exporting (saving) the result as a json file. +**Unpublished configuration** β€” `config variables list` with `--packageKeys`: + +```bash +content-cli config variables list -p --packageKeys [ ...] +``` + +Use `--json` with either flow to export the array to a file (see **Output** above). ### Listing Assignments diff --git a/src/commands/configuration-management/api/staging-package-variables-api.ts b/src/commands/configuration-management/api/staging-package-variables-api.ts new file mode 100644 index 0000000..dd052c7 --- /dev/null +++ b/src/commands/configuration-management/api/staging-package-variables-api.ts @@ -0,0 +1,21 @@ +import { Context } from "../../../core/command/cli-context"; +import { FatalError } from "../../../core/utils/logger"; +import { HttpClient } from "../../../core/http/http-client"; +import { StagingVariableManifestTransport } from "../interfaces/package-export.interfaces"; + +export class StagingPackageVariablesApi { + private httpClient: () => HttpClient; + + constructor(context: Context) { + this.httpClient = () => context.httpClient; + } + + public async findAllByPackageKeys(packageKeys: string[]): Promise { + const path = `/pacman/api/core/staging/packages/variables/by-package-keys`; + return await this.httpClient() + .post(path, packageKeys) + .catch(e => { + throw new FatalError(`Problem listing staging variables for packages: ${e}`); + }); + } +} diff --git a/src/commands/configuration-management/config-command.service.ts b/src/commands/configuration-management/config-command.service.ts index b0a903f..9716df4 100644 --- a/src/commands/configuration-management/config-command.service.ts +++ b/src/commands/configuration-management/config-command.service.ts @@ -31,8 +31,19 @@ export class ConfigCommandService { } } - public async listVariables(jsonResponse: boolean, keysByVersion: string[], keysByVersionFile: string): Promise { - if (jsonResponse) { + public async listVariables( + jsonResponse: boolean, + keysByVersion: string[], + keysByVersionFile: string, + packageKeys: string[] + ): Promise { + if (packageKeys.length > 0) { + if (jsonResponse) { + await this.variableService.exportStagingVariables(packageKeys); + } else { + await this.variableService.listStagingVariables(packageKeys); + } + } else if (jsonResponse) { await this.variableService.exportVariables(keysByVersion, keysByVersionFile); } else { await this.variableService.listVariables(keysByVersion, keysByVersionFile); diff --git a/src/commands/configuration-management/interfaces/package-export.interfaces.ts b/src/commands/configuration-management/interfaces/package-export.interfaces.ts index 6e8a52b..8830bf4 100644 --- a/src/commands/configuration-management/interfaces/package-export.interfaces.ts +++ b/src/commands/configuration-management/interfaces/package-export.interfaces.ts @@ -49,6 +49,11 @@ export interface VariableManifestTransport { variables?: VariableExportTransport[]; } +export interface StagingVariableManifestTransport { + packageKey: string; + variables?: VariableExportTransport[]; +} + export interface PackageKeyAndVersionPair { packageKey: string; version: string; diff --git a/src/commands/configuration-management/module.ts b/src/commands/configuration-management/module.ts index a5b93bc..230c961 100644 --- a/src/commands/configuration-management/module.ts +++ b/src/commands/configuration-management/module.ts @@ -87,9 +87,10 @@ class Module extends IModule { .description("Commands related to variable configs"); variablesCommand.command("list") - .description("Command to list versioned variables of packages") + .description("List package variables: use --packageKeys for staging (unpublished), or --keysByVersion / --keysByVersionFile for versioned packages") .option("--json", "Return response as json type", "") - .option("--keysByVersion ", "Mapping of package keys and versions", "") + .option("--packageKeys ", "Package keys (staging variables only; mutually exclusive with versioned options)", []) + .option("--keysByVersion ", "Mapping of package keys and versions", []) .option("--keysByVersionFile ", "Package keys by version mappings file path.", "") .action(this.listVariables); @@ -203,7 +204,27 @@ class Module extends IModule { } private async listVariables(context: Context, command: Command, options: OptionValues): Promise { - await new ConfigCommandService(context).listVariables(options.json, options.keysByVersion, options.keysByVersionFile); + const hasStagingKeys = options.packageKeys.length > 0; + const hasVersioned = + options.keysByVersion.length > 0 || options.keysByVersionFile !== ""; + + if (hasStagingKeys && hasVersioned) { + throw new Error( + "Please provide either --packageKeys or --keysByVersion/--keysByVersionFile, but not both." + ); + } + if (!hasStagingKeys && !hasVersioned) { + throw new Error( + "Please provide --packageKeys for staging variables, or --keysByVersion / --keysByVersionFile for versioned packages." + ); + } + + await new ConfigCommandService(context).listVariables( + options.json, + options.keysByVersion, + options.keysByVersionFile, + options.packageKeys + ); } private async listAssignments(context: Context, command: Command, options: OptionValues): Promise { diff --git a/src/commands/configuration-management/variable.service.ts b/src/commands/configuration-management/variable.service.ts index 77c8b85..b24633f 100644 --- a/src/commands/configuration-management/variable.service.ts +++ b/src/commands/configuration-management/variable.service.ts @@ -3,20 +3,23 @@ import { Context } from "../../core/command/cli-context"; import { FatalError, logger } from "../../core/utils/logger"; import { StudioService } from "./studio.service"; import { FileService, fileService } from "../../core/utils/file-service"; -import { PackageKeyAndVersionPair, VariableManifestTransport } from "./interfaces/package-export.interfaces"; +import { PackageKeyAndVersionPair, StagingVariableManifestTransport, VariableManifestTransport } from "./interfaces/package-export.interfaces"; import { BatchImportExportApi } from "./api/batch-import-export-api"; import { URLSearchParams } from "url"; import { VariableAssignmentCandidatesApi } from "./api/variable-assignment-candidates-api"; +import { StagingPackageVariablesApi } from "./api/staging-package-variables-api"; export class VariableService { private batchImportExportApi: BatchImportExportApi; private variableAssignmentCandidatesApi: VariableAssignmentCandidatesApi; + private stagingPackageVariablesApi: StagingPackageVariablesApi; private studioService: StudioService; constructor(context: Context) { this.batchImportExportApi = new BatchImportExportApi(context); this.variableAssignmentCandidatesApi = new VariableAssignmentCandidatesApi(context); + this.stagingPackageVariablesApi = new StagingPackageVariablesApi(context); this.studioService = new StudioService(context); } @@ -50,6 +53,28 @@ export class VariableService { this.exportToJson(variableManifests); } + public async listStagingVariables(packageKeys: string[]): Promise { + const byPackage = await this.fetchStagingVariablesByPackageKeys(packageKeys); + byPackage.forEach(entry => { + logger.info(JSON.stringify(entry)); + }); + } + + public async exportStagingVariables(packageKeys: string[]): Promise { + const byPackage = await this.fetchStagingVariablesByPackageKeys(packageKeys); + this.exportToJson(byPackage); + } + + private async fetchStagingVariablesByPackageKeys( + packageKeys: string[] + ): Promise { + if (packageKeys.length === 0) { + throw new FatalError("Please provide at least one package key!"); + } + + return await this.stagingPackageVariablesApi.findAllByPackageKeys(packageKeys); + } + private async getVersionedVariablesByKeyVersionPairs(keysByVersion: string[], keysByVersionFile: string): Promise { const variablesExportRequest: PackageKeyAndVersionPair[] = await this.buildKeyVersionPairs(keysByVersion, keysByVersionFile); diff --git a/tests/commands/configuration-management/config-list-variables.spec.ts b/tests/commands/configuration-management/config-list-variables.spec.ts index 9c028a9..1578055 100644 --- a/tests/commands/configuration-management/config-list-variables.spec.ts +++ b/tests/commands/configuration-management/config-list-variables.spec.ts @@ -3,6 +3,8 @@ import * as fs from "fs"; import { parse } from "../../../src/core/utils/json"; import { PackageKeyAndVersionPair, + StagingVariableManifestTransport, + VariableExportTransport, VariableManifestTransport, } from "../../../src/commands/configuration-management/interfaces/package-export.interfaces"; import { PackageManagerVariableType } from "../../../src/commands/studio/interfaces/package-manager.interfaces"; @@ -112,7 +114,7 @@ describe("Config listVariables", () => { }) it("Should list fixed variables for non-json response", async () => { - await new ConfigCommandService(testContext).listVariables(false, ["key-1:1.0.0", "key-2:1.0.0", "key-3:1.0.0"], null); + await new ConfigCommandService(testContext).listVariables(false, ["key-1:1.0.0", "key-2:1.0.0", "key-3:1.0.0"], "", []); expect(loggingTestTransport.logMessages.length).toBe(3); expect(loggingTestTransport.logMessages[0].message).toContain(JSON.stringify(fixedVariableManifests[0])); @@ -124,7 +126,7 @@ describe("Config listVariables", () => { }) it("Should export fixed variables for json response", async () => { - await new ConfigCommandService(testContext).listVariables(true, ["key-1:1.0.0", "key-2:1.0.0", "key-3:1.0.0"], null); + await new ConfigCommandService(testContext).listVariables(true, ["key-1:1.0.0", "key-2:1.0.0", "key-3:1.0.0"], "", []); expect(loggingTestTransport.logMessages.length).toBe(1); expect(loggingTestTransport.logMessages[0].message).toContain(FileService.fileDownloadedMessage); @@ -140,7 +142,7 @@ describe("Config listVariables", () => { (fs.existsSync as jest.Mock).mockReturnValue(true); (fs.readFileSync as jest.Mock).mockReturnValue(JSON.stringify(packageKeyAndVersionPairs)); - await new ConfigCommandService(testContext).listVariables(false, [], "key_version_mapping.json"); + await new ConfigCommandService(testContext).listVariables(false, [], "key_version_mapping.json", []); expect(loggingTestTransport.logMessages.length).toBe(3); expect(loggingTestTransport.logMessages[0].message).toContain(JSON.stringify(fixedVariableManifests[0])); @@ -155,7 +157,7 @@ describe("Config listVariables", () => { (fs.existsSync as jest.Mock).mockReturnValue(true); (fs.readFileSync as jest.Mock).mockReturnValue(JSON.stringify(packageKeyAndVersionPairs)); - await new ConfigCommandService(testContext).listVariables(true, [], "key_version_mapping.json"); + await new ConfigCommandService(testContext).listVariables(true, [], "key_version_mapping.json", []); expect(loggingTestTransport.logMessages.length).toBe(1); expect(loggingTestTransport.logMessages[0].message).toContain(FileService.fileDownloadedMessage); @@ -169,9 +171,66 @@ describe("Config listVariables", () => { it("Should throw error if no mapping and no file path is provided", async () => { try { - await new ConfigCommandService(testContext).listVariables(true, [], ""); + await new ConfigCommandService(testContext).listVariables(true, [], "", []); } catch (e) { expect(e.message).toEqual("Please provide keysByVersion mappings or file path!"); } }) -}) \ No newline at end of file + + describe("staging variables via --packageKeys", () => { + const stagingVariablesByPackageKeysBaseUrl = + `${testContext.profile.team.replace(/\/$/, "")}/pacman/api/core/staging/packages/variables/by-package-keys`; + + const stagingVarsPkgA: VariableExportTransport[] = [ + { key: "DATA_POOL", type: "SINGLE_VALUE", value: "pool-id-1", metadata: {} }, + { key: "OTHER", type: "CONNECTION", value: { connectionId: "c1" }, metadata: {} }, + ]; + const stagingVarsPkgB: VariableExportTransport[] = [ + { key: "DATA_POOL", type: "SINGLE_VALUE", value: "pool-id-2", metadata: {} }, + ]; + + const batchResponse: StagingVariableManifestTransport[] = [ + { packageKey: "pkg-a", variables: stagingVarsPkgA }, + { packageKey: "pkg-b", variables: stagingVarsPkgB }, + ]; + + const expectedPackageKeys = ["pkg-a", "pkg-b"]; + + it("Should list staging variables for non-json response", async () => { + const url = stagingVariablesByPackageKeysBaseUrl; + mockAxiosPost(url, batchResponse); + + await new ConfigCommandService(testContext).listVariables(false, [], "", expectedPackageKeys); + + expect(loggingTestTransport.logMessages.length).toBe(2); + expect(loggingTestTransport.logMessages[0].message).toContain(JSON.stringify(batchResponse[0])); + expect(loggingTestTransport.logMessages[1].message).toContain(JSON.stringify(batchResponse[1])); + + const postBody = parse(mockedPostRequestBodyByUrl.get(url)); + expect(postBody).toEqual(expectedPackageKeys); + }); + + it("Should export staging variables for json response", async () => { + const pkgAOnlyResponse: StagingVariableManifestTransport[] = [ + { packageKey: "pkg-a", variables: stagingVarsPkgA }, + ]; + const url = stagingVariablesByPackageKeysBaseUrl; + mockAxiosPost(url, pkgAOnlyResponse); + + await new ConfigCommandService(testContext).listVariables(true, [], "", ["pkg-a"]); + + expect(loggingTestTransport.logMessages.length).toBe(1); + expect(loggingTestTransport.logMessages[0].message).toContain(FileService.fileDownloadedMessage); + + const expectedFileName = loggingTestTransport.logMessages[0].message.split(FileService.fileDownloadedMessage)[1]; + expect(mockWriteFileSync).toHaveBeenCalledWith( + path.resolve(process.cwd(), expectedFileName), + JSON.stringify(pkgAOnlyResponse), + {encoding: "utf-8"} + ); + + const postBody = parse(mockedPostRequestBodyByUrl.get(url)); + expect(postBody).toEqual(["pkg-a"]); + }); + }); +}); diff --git a/tests/commands/configuration-management/module.spec.ts b/tests/commands/configuration-management/module.spec.ts index 1d674dc..c8cbb1f 100644 --- a/tests/commands/configuration-management/module.spec.ts +++ b/tests/commands/configuration-management/module.spec.ts @@ -9,6 +9,13 @@ jest.mock("../../../src/commands/configuration-management/config-command.service jest.mock("../../../src/commands/configuration-management/node-dependency.service"); jest.mock("../../../src/commands/configuration-management/package-version-command.service"); +/** Mirrors default values on `config variables list` Commander options (keep in sync with module.ts). */ +const variablesListOptionDefaults: OptionValues = { + packageKeys: [], + keysByVersion: [], + keysByVersionFile: "", +}; + describe("Configuration Management Module - Action Validations", () => { let module: Module; let mockCommand: Command; @@ -22,6 +29,7 @@ describe("Configuration Management Module - Action Validations", () => { mockConfigCommandService = { listPackages: jest.fn().mockResolvedValue(undefined), + listVariables: jest.fn().mockResolvedValue(undefined), batchExportPackages: jest.fn().mockResolvedValue(undefined), batchImportPackages: jest.fn().mockResolvedValue(undefined), } as any; @@ -406,6 +414,86 @@ describe("Configuration Management Module - Action Validations", () => { }); }); + describe("listVariables validation", () => { + it("should throw when --packageKeys and --keysByVersion are both provided", async () => { + const options: OptionValues = { + ...variablesListOptionDefaults, + packageKeys: ["pkg-a"], + keysByVersion: ["key-1:1.0.0"], + }; + + await expect( + (module as any).listVariables(testContext, mockCommand, options) + ).rejects.toThrow( + "Please provide either --packageKeys or --keysByVersion/--keysByVersionFile, but not both." + ); + + expect(mockConfigCommandService.listVariables).not.toHaveBeenCalled(); + }); + + it("should throw when --packageKeys and --keysByVersionFile are both provided", async () => { + const options: OptionValues = { + ...variablesListOptionDefaults, + packageKeys: ["pkg-a"], + keysByVersionFile: "mapping.json", + }; + + await expect( + (module as any).listVariables(testContext, mockCommand, options) + ).rejects.toThrow( + "Please provide either --packageKeys or --keysByVersion/--keysByVersionFile, but not both." + ); + + expect(mockConfigCommandService.listVariables).not.toHaveBeenCalled(); + }); + + it("should throw when neither staging nor versioned inputs are provided", async () => { + const options: OptionValues = {...variablesListOptionDefaults}; + + await expect( + (module as any).listVariables(testContext, mockCommand, options) + ).rejects.toThrow( + "Please provide --packageKeys for staging variables, or --keysByVersion / --keysByVersionFile for versioned packages." + ); + + expect(mockConfigCommandService.listVariables).not.toHaveBeenCalled(); + }); + + it("should call listVariables for staging when only --packageKeys is provided", async () => { + const options: OptionValues = { + ...variablesListOptionDefaults, + packageKeys: ["pkg-a", "pkg-b"], + json: true, + }; + + await (module as any).listVariables(testContext, mockCommand, options); + + expect(mockConfigCommandService.listVariables).toHaveBeenCalledWith( + true, + [], + "", + ["pkg-a", "pkg-b"] + ); + }); + + it("should call listVariables for versioned when only --keysByVersion is provided", async () => { + const options: OptionValues = { + ...variablesListOptionDefaults, + keysByVersion: ["k:v"], + json: false, + }; + + await (module as any).listVariables(testContext, mockCommand, options); + + expect(mockConfigCommandService.listVariables).toHaveBeenCalledWith( + false, + ["k:v"], + "", + [] + ); + }); + }); + describe("createPackageVersion validation", () => { let mockPackageVersionCommandService: jest.Mocked;