Skip to content

Commit

Permalink
Merge pull request #184 from celonis/Buqeta/ta-3012-call-packages-dif…
Browse files Browse the repository at this point in the history
…f-with-wip-from-content-cli

TA-3012: Add diff packages command
  • Loading branch information
Buqeta authored May 30, 2024
2 parents 1807f22 + d0d696d commit c1f6e03
Show file tree
Hide file tree
Showing 8 changed files with 315 additions and 1 deletion.
7 changes: 7 additions & 0 deletions jest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,13 @@ const config: Config.InitialOptions = {
setupFilesAfterEnv: [
"<rootDir>/tests/jest.setup.ts",
],
globals: {
"ts-jest": {
tsconfig: {
sourceMap: true
}
}
}
}

export default config
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@celonis/content-cli",
"version": "0.8.3",
"version": "0.8.4",
"description": "CLI Tool to help manage content in Celonis EMS",
"main": "content-cli.js",
"bin": {
Expand Down
23 changes: 23 additions & 0 deletions src/api/diff-api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import {PackageDiffTransport} from "../interfaces/diff-package.transport";
import {httpClientV2} from "../services/http-client-service.v2";
import * as FormData from "form-data";

class DiffApi {
public static readonly INSTANCE = new DiffApi();

public async diffPackages(data: FormData): Promise<PackageDiffTransport[]> {
return httpClientV2.postFile(
"/package-manager/api/core/packages/diff/configuration",
data
);
}

public async hasChanges(data: FormData): Promise<PackageDiffTransport[]> {
return httpClientV2.postFile(
"/package-manager/api/core/packages/diff/configuration/has-changes",
data
);
}
}

export const diffApi = DiffApi.INSTANCE;
5 changes: 5 additions & 0 deletions src/commands/config.command.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {batchImportExportService} from "../services/package-manager/batch-import-export-service";
import {variableService} from "../services/package-manager/variable-service";
import {diffService} from "../services/package-manager/diff-service";

export class ConfigCommand {

Expand All @@ -26,4 +27,8 @@ export class ConfigCommand {
public batchImportPackages(file: string, overwrite: boolean): Promise<void> {
return batchImportExportService.batchImportPackages(file, overwrite);
}

public diffPackages(file: string, hasChanges: boolean, jsonResponse: boolean): Promise<void> {
return diffService.diffPackages(file, hasChanges, jsonResponse);
}
}
17 changes: 17 additions & 0 deletions src/content-cli-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,13 +70,30 @@ export class Config {

return program;
}

public static diff(program: CommanderStatic): CommanderStatic {
program
.command("diff")
.description("Command to diff configs of packages")
.option("-p, --profile <profile>", "Profile of the team/realm which you want to use to diff the packages with")
.option("--hasChanges", "Flag to return only the information if the package has changes without the actual changes")
.option("--json", "Return the response as a JSON file")
.requiredOption("-f, --file <file>", "Exported packages file (relative or absolute path)")
.action(async cmd => {
await new ConfigCommand().diffPackages(cmd.file, cmd.hasChanges, cmd.json);
process.exit();
});

return program;
}
}

const loadAllCommands = () => {
Config.list(commander);
Config.listVariables(commander);
Config.export(commander);
Config.import(commander);
Config.diff(commander)
commander.parse(process.argv);
};

Expand Down
18 changes: 18 additions & 0 deletions src/interfaces/diff-package.transport.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
export interface ConfigurationChangeTransport {
op: string;
path: string;
from: string;
value: object;
}

export interface NodeDiffTransport {
nodeKey: string;
changes: ConfigurationChangeTransport[];
}

export interface PackageDiffTransport {
packageKey: string;
hasChanges: boolean
packageChanges: ConfigurationChangeTransport[];
nodesWithChanges: NodeDiffTransport[];
}
73 changes: 73 additions & 0 deletions src/services/package-manager/diff-service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import * as AdmZip from "adm-zip";
import {Readable} from "stream";
import * as FormData from "form-data";
import {diffApi} from "../../api/diff-api";
import {FileService, fileService} from "../file-service";
import {logger} from "../../util/logger";
import {PackageDiffTransport} from "../../interfaces/diff-package.transport";
import {v4 as uuidv4} from "uuid";

class DiffService {

public async diffPackages(file: string, hasChanges: boolean, jsonResponse: boolean): Promise<void> {
if (hasChanges) {
await this.hasChanges(file, jsonResponse);
} else {
await this.diffPackagesAndReturnDiff(file, jsonResponse);
}
}

private async hasChanges(file: string, jsonResponse: boolean): Promise<void> {
const packages = new AdmZip(file);
const formData = this.buildBodyForDiff(packages);
const returnedHasChangesData = await diffApi.hasChanges(formData);

if (jsonResponse) {
this.exportListOfPackageDiffs(returnedHasChangesData);
} else {
logger.info(this.buildStringResponse(returnedHasChangesData));
}
}

private async diffPackagesAndReturnDiff(file: string, jsonResponse: boolean): Promise<void> {
const packages = new AdmZip(file);
const formData = this.buildBodyForDiff(packages);
const returnedHasChangesData = await diffApi.diffPackages(formData);

if (jsonResponse) {
this.exportListOfPackageDiffs(returnedHasChangesData);
} else {
logger.info(this.buildStringResponse(returnedHasChangesData));
}
}

private buildBodyForDiff(packages: AdmZip): FormData {
const formData = new FormData();
const readableStream = this.getReadableStream(packages);

formData.append("file", readableStream, {filename: "packages.zip"});

return formData;
}

private getReadableStream(packages: AdmZip): Readable {
return new Readable({
read(): void {
this.push(packages.toBuffer());
this.push(null);
}
});
}

private exportListOfPackageDiffs(packageDiffs: PackageDiffTransport[]): void {
const filename = uuidv4() + ".json";
fileService.writeToFileWithGivenName(JSON.stringify(packageDiffs), filename);
logger.info(FileService.fileDownloadedMessage + filename);
}

private buildStringResponse(packageDiffs: PackageDiffTransport[]): string {
return "\n" + JSON.stringify(packageDiffs, null, 2);
}
}

export const diffService = new DiffService();
171 changes: 171 additions & 0 deletions tests/config/config-diff.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
import {PackageManifestTransport} from "../../src/interfaces/package-export-transport";
import {ConfigUtils} from "../utls/config-utils";
import * as path from "path";
import {stringify} from "../../src/util/yaml";
import {mockCreateReadStream, mockExistsSync, mockReadFileSync} from "../utls/fs-mock-utils";
import {
PackageDiffTransport
} from "../../src/interfaces/diff-package.transport";
import {mockAxiosPost} from "../utls/http-requests-mock";
import {ConfigCommand} from "../../src/commands/config.command";
import { mockWriteFileSync, mockWriteSync, testTransport } from "../jest.setup";
import { FileService } from "../../src/services/file-service";

describe("Config diff", () => {

beforeEach(() => {
mockExistsSync();
});

it("Should show on terminal if packages have changes with hasChanges set to true and jsonResponse false", async () => {
const manifest: PackageManifestTransport[] = [];
manifest.push(ConfigUtils.buildManifestForKeyAndFlavor("package-key", "STUDIO"));

const firstPackageNode = ConfigUtils.buildPackageNode("package-key", stringify({metadata: {description: "test"}, variables: [], dependencies: []}));
const firstChildNode = ConfigUtils.buildChildNode("key-1", "package-key", "TEST");
const firstPackageZip = ConfigUtils.buildExportPackageZip(firstPackageNode, [firstChildNode], "1.0.0");
const exportedPackagesZip = ConfigUtils.buildBatchExportZip(manifest, [firstPackageZip]);

mockReadFileSync(exportedPackagesZip.toBuffer());
mockCreateReadStream(exportedPackagesZip.toBuffer());

const diffResponse: PackageDiffTransport[] = [{
packageKey: "package-key",
hasChanges: true,
packageChanges: [],
nodesWithChanges: []
}];

mockAxiosPost("https://myTeam.celonis.cloud/package-manager/api/core/packages/diff/configuration/has-changes", diffResponse);

await new ConfigCommand().diffPackages("./packages.zip", true, false);

expect(testTransport.logMessages.length).toBe(1);
expect(testTransport.logMessages[0].message).toContain(
JSON.stringify(diffResponse, null, 2)
);
});

it("Should show diff on terminal with hasChanges set to false and jsonResponse false", async () => {
const manifest: PackageManifestTransport[] = [];
manifest.push(ConfigUtils.buildManifestForKeyAndFlavor("package-key", "STUDIO"));

const firstPackageNode = ConfigUtils.buildPackageNode("package-key", stringify({metadata: {description: "test"}, variables: [], dependencies: []}));
const firstChildNode = ConfigUtils.buildChildNode("key-1", "package-key", "TEST");
const firstPackageZip = ConfigUtils.buildExportPackageZip(firstPackageNode, [firstChildNode], "1.0.0");
const exportedPackagesZip = ConfigUtils.buildBatchExportZip(manifest, [firstPackageZip]);

mockReadFileSync(exportedPackagesZip.toBuffer());
mockCreateReadStream(exportedPackagesZip.toBuffer());

const diffResponse: PackageDiffTransport[] = [{
packageKey: "package-key",
hasChanges: true,
packageChanges: [
{
op: "add",
path: "/test",
from: "bbbb",
value: JSON.parse("123")
}],
nodesWithChanges: [{
nodeKey: "key-1",
changes: [{
op: "add",
path: "/test",
from: "bbb",
value: JSON.parse("234")
}]
}]
}];

mockAxiosPost("https://myTeam.celonis.cloud/package-manager/api/core/packages/diff/configuration", diffResponse);

await new ConfigCommand().diffPackages("./packages.zip", false, false);

expect(testTransport.logMessages.length).toBe(1);
expect(testTransport.logMessages[0].message).toContain(
JSON.stringify(diffResponse, null, 2)
);
});

it("Should generate a json file with diff info when hasChanges is set to false and jsonResponse is set to true", async () => {
const manifest: PackageManifestTransport[] = [];
manifest.push(ConfigUtils.buildManifestForKeyAndFlavor("package-key", "STUDIO"));

const firstPackageNode = ConfigUtils.buildPackageNode("package-key", stringify({metadata: {description: "test"}, variables: [], dependencies: []}));
const firstChildNode = ConfigUtils.buildChildNode("key-1", "package-key", "TEST");
const firstPackageZip = ConfigUtils.buildExportPackageZip(firstPackageNode, [firstChildNode], "1.0.0");
const exportedPackagesZip = ConfigUtils.buildBatchExportZip(manifest, [firstPackageZip]);

mockReadFileSync(exportedPackagesZip.toBuffer());
mockCreateReadStream(exportedPackagesZip.toBuffer());

const diffResponse: PackageDiffTransport[] = [{
packageKey: "package-key",
hasChanges: true,
packageChanges: [
{
op: "add",
path: "/test",
from: "bbbb",
value: JSON.parse("123")
}],
nodesWithChanges: [{
nodeKey: "key-1",
changes: [{
op: "add",
path: "/test",
from: "bbb",
value: JSON.parse("234")
}]
}]
}];

mockAxiosPost("https://myTeam.celonis.cloud/package-manager/api/core/packages/diff/configuration", diffResponse);

await new ConfigCommand().diffPackages("./packages.zip", false, true);

const expectedFileName = testTransport.logMessages[0].message.split(FileService.fileDownloadedMessage)[1];

expect(mockWriteFileSync).toHaveBeenCalledWith(path.resolve(process.cwd(), expectedFileName), expect.any(String), {encoding: "utf-8"});
const exportedPackageDiffTransport = JSON.parse(mockWriteFileSync.mock.calls[0][1]) as PackageDiffTransport[];
expect(exportedPackageDiffTransport.length).toBe(1);

const exportedFirstPackageDiffTransport = exportedPackageDiffTransport.filter(diffTransport => diffTransport.packageKey === firstPackageNode.key);
expect(exportedFirstPackageDiffTransport).toEqual(diffResponse);
});

it("Should generate a json file with info whether packages have changes when hasChanges is set to true and jsonResponse is set to true", async () => {
const manifest: PackageManifestTransport[] = [];
manifest.push(ConfigUtils.buildManifestForKeyAndFlavor("package-key", "STUDIO"));

const firstPackageNode = ConfigUtils.buildPackageNode("package-key", stringify({metadata: {description: "test"}, variables: [], dependencies: []}));
const firstChildNode = ConfigUtils.buildChildNode("key-1", "package-key", "TEST");
const firstPackageZip = ConfigUtils.buildExportPackageZip(firstPackageNode, [firstChildNode], "1.0.0");
const exportedPackagesZip = ConfigUtils.buildBatchExportZip(manifest, [firstPackageZip]);

mockReadFileSync(exportedPackagesZip.toBuffer());
mockCreateReadStream(exportedPackagesZip.toBuffer());

const diffResponse: PackageDiffTransport[] = [{
packageKey: "package-key",
hasChanges: true,
packageChanges: [],
nodesWithChanges: []
}];

mockAxiosPost("https://myTeam.celonis.cloud/package-manager/api/core/packages/diff/configuration/has-changes", diffResponse);

await new ConfigCommand().diffPackages("./packages.zip", true, true);

const expectedFileName = testTransport.logMessages[0].message.split(FileService.fileDownloadedMessage)[1];

expect(mockWriteFileSync).toHaveBeenCalledWith(path.resolve(process.cwd(), expectedFileName), expect.any(String), {encoding: "utf-8"});
const exportedPackageDiffTransport = JSON.parse(mockWriteFileSync.mock.calls[0][1]) as PackageDiffTransport[];
expect(exportedPackageDiffTransport.length).toBe(1);

const exportedFirstPackageDiffTransport = exportedPackageDiffTransport.filter(diffTransport => diffTransport.packageKey === firstPackageNode.key);
expect(exportedFirstPackageDiffTransport).toEqual(diffResponse);
});
});

0 comments on commit c1f6e03

Please sign in to comment.