Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 32 additions & 1 deletion src/commands/configuration-management/api/node-api.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { HttpClient } from "../../../core/http/http-client";
import { Context } from "../../../core/command/cli-context";
import { NodeTransport } from "../interfaces/node.interfaces";
import { NodeTransport, SaveNodeTransport, UpdateNodeTransport } from "../interfaces/node.interfaces";
import { FatalError } from "../../../core/utils/logger";

export class NodeApi {
Expand Down Expand Up @@ -33,6 +33,37 @@ export class NodeApi {
});
}

public async createStagingNode(packageKey: string, transport: SaveNodeTransport, validateOnly: boolean): Promise<NodeTransport | void> {
const suffix = validateOnly ? "?validate=true" : "";

return this.httpClient()
.post(`/pacman/api/core/staging/packages/${packageKey}/nodes${suffix}`, transport)
.catch((e) => {
throw new FatalError(`Problem creating node in package ${packageKey}: ${e}`);
});
}

public async updateStagingNode(packageKey: string, nodeKey: string, transport: UpdateNodeTransport, validateOnly: boolean): Promise<NodeTransport | void> {
const suffix = validateOnly ? "?validate=true" : "";

return this.httpClient()
.put(`/pacman/api/core/staging/packages/${packageKey}/nodes/${nodeKey}${suffix}`, transport)
.catch((e) => {
throw new FatalError(`Problem updating node ${nodeKey} in package ${packageKey}: ${e}`);
});
}

public async archiveStagingNode(packageKey: string, nodeKey: string, force: boolean): Promise<void> {
const queryParams = new URLSearchParams();
queryParams.set("force", force.toString());

return this.httpClient()
.delete(`/pacman/api/core/staging/packages/${packageKey}/nodes/${nodeKey}/archive?${queryParams.toString()}`)
.catch((e) => {
throw new FatalError(`Problem archiving node ${nodeKey} in package ${packageKey}: ${e}`);
});
}

public async findVersionedNodesByPackage(packageKey: string, version: string, withConfiguration: boolean, limit: number, offset: number): Promise<NodeTransport[]> {
const queryParams = new URLSearchParams();
queryParams.set("version", version);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,25 @@ export interface NodeTransport {
schemaVersion: number;
flavor?: string;
}

export interface SaveNodeTransport {
key: string;
name: string;
parentNodeKey: string;
type: string;
configuration?: NodeConfiguration;
invalidConfiguration?: string;
invalidContent?: boolean;
schemaVersion?: number;
[key: string]: any;
}

export interface UpdateNodeTransport {
name: string;
parentNodeKey: string;
configuration?: NodeConfiguration;
invalidConfiguration?: string;
invalidContent?: boolean;
schemaVersion?: number;
[key: string]: any;
}
54 changes: 54 additions & 0 deletions src/commands/configuration-management/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import { NodeDependencyService } from "./node-dependency.service";
import { PackageVersionCommandService } from "./package-version-command.service";
import { PackageValidationService } from "./package-validation.service";
import { fileService } from "../../core/utils/file-service";

class Module extends IModule {

Expand Down Expand Up @@ -116,6 +117,32 @@
.option("--json", "Return the response as a JSON file")
.action(this.findNode);

nodesCommand.command("create")
.description("Create a new staging node in a package")
.requiredOption("--packageKey <packageKey>", "Identifier of the package")
.option("--body <body>", "Node payload as JSON string")
.option("-f, --file <file>", "Path to a JSON file containing the node payload")
.option("--validate", "Only validate the payload without persisting. Returns success if valid.", false)
.option("--json", "Return the response as a JSON file")
.action(this.createNode);

nodesCommand.command("update")
.description("Update a staging node in a package")
.requiredOption("--packageKey <packageKey>", "Identifier of the package")
.requiredOption("--nodeKey <nodeKey>", "Identifier of the node")
.option("--body <body>", "Node payload as JSON string")
.option("-f, --file <file>", "Path to a JSON file containing the node payload")
.option("--validate", "Only validate the payload without persisting. Returns success if valid.", false)
.option("--json", "Return the response as a JSON file")
.action(this.updateNode);

nodesCommand.command("delete")
.description("Delete (archive) a staging node in a package")
.requiredOption("--packageKey <packageKey>", "Identifier of the package")
.requiredOption("--nodeKey <nodeKey>", "Identifier of the node")
.option("--force", "Force delete even if the node has dependants", false)
.action(this.archiveNode);

nodesCommand.command("list")
.description("List nodes in a specific package version")
.requiredOption("--packageKey <packageKey>", "Identifier of the package")
Expand Down Expand Up @@ -249,6 +276,33 @@
await new NodeService(context).findNode(options.packageKey, options.nodeKey, options.withConfiguration, options.packageVersion ?? null, options.json);
}

private async createNode(context: Context, command: Command, options: OptionValues): Promise<void> {
const body = Module.resolveBody(options.body, options.file);
await new NodeService(context).createNode(options.packageKey, body, options.validate, options.json);
}

private async updateNode(context: Context, command: Command, options: OptionValues): Promise<void> {
const body = Module.resolveBody(options.body, options.file);
await new NodeService(context).updateNode(options.packageKey, options.nodeKey, body, options.validate, options.json);
}

private static resolveBody(body: string | undefined, file: string | undefined): string {
if (body && file) {
throw new Error("Please provide either --body or --file, but not both.");
}
if (!body && !file) {
throw new Error("Please provide either --body or --file.");
}
if (file) {
return fileService.readFile(file);
}
return body!;

Check warning on line 299 in src/commands/configuration-management/module.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

This assertion is unnecessary since it does not change the type of the expression.

See more on https://sonarcloud.io/project/issues?id=celonis_content-cli&issues=AZ2uzTRz9oD0_rp76E0N&open=AZ2uzTRz9oD0_rp76E0N&pullRequest=344
}

private async archiveNode(context: Context, command: Command, options: OptionValues): Promise<void> {
await new NodeService(context).archiveNode(options.packageKey, options.nodeKey, options.force);
}

private async listNodes(context: Context, command: Command, options: OptionValues): Promise<void> {
await new NodeService(context).listNodes(options.packageKey, options.packageVersion, options.limit, options.offset, options.withConfiguration, options.json);
}
Expand Down
43 changes: 42 additions & 1 deletion src/commands/configuration-management/node.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Context } from "../../core/command/cli-context";
import { fileService, FileService } from "../../core/utils/file-service";
import { logger } from "../../core/utils/logger";
import { v4 as uuidv4 } from "uuid";
import { NodeTransport } from "./interfaces/node.interfaces";
import { NodeTransport, SaveNodeTransport, UpdateNodeTransport } from "./interfaces/node.interfaces";

export class NodeService {
private nodeApi: NodeApi;
Expand Down Expand Up @@ -40,6 +40,47 @@ export class NodeService {
}
}

public async createNode(packageKey: string, body: string, validateOnly: boolean, jsonResponse: boolean): Promise<void> {
const transport: SaveNodeTransport = JSON.parse(body);
const node = await this.nodeApi.createStagingNode(packageKey, transport, validateOnly);

if (validateOnly) {
logger.info(`Validation successful for node ${transport.key} in package ${packageKey}.`);
return;
}

if (jsonResponse) {
const filename = uuidv4() + ".json";
fileService.writeToFileWithGivenName(JSON.stringify(node, null, 2), filename);
logger.info(FileService.fileDownloadedMessage + filename);
} else {
this.printNode(node as NodeTransport);
}
}

public async updateNode(packageKey: string, nodeKey: string, body: string, validateOnly: boolean, jsonResponse: boolean): Promise<void> {
const transport: UpdateNodeTransport = JSON.parse(body);
const node = await this.nodeApi.updateStagingNode(packageKey, nodeKey, transport, validateOnly);

if (validateOnly) {
logger.info(`Validation successful for node ${nodeKey} in package ${packageKey}.`);
return;
}

if (jsonResponse) {
const filename = uuidv4() + ".json";
fileService.writeToFileWithGivenName(JSON.stringify(node, null, 2), filename);
logger.info(FileService.fileDownloadedMessage + filename);
} else {
this.printNode(node as NodeTransport);
}
}

public async archiveNode(packageKey: string, nodeKey: string, force: boolean): Promise<void> {
await this.nodeApi.archiveStagingNode(packageKey, nodeKey, force);
logger.info(`Node ${nodeKey} in package ${packageKey} archived successfully.`);
}

private printNode(node: NodeTransport): void {
logger.info(`ID: ${node.id}`);
logger.info(`Key: ${node.key}`);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { mockAxiosDelete } from "../../utls/http-requests-mock";
import { NodeService } from "../../../src/commands/configuration-management/node.service";
import { testContext } from "../../utls/test-context";
import { loggingTestTransport } from "../../jest.setup";

describe("Node archive", () => {
const packageKey = "package-key";
const nodeKey = "node-key";

it("Should archive node without force", async () => {
const apiUrl = `https://myTeam.celonis.cloud/pacman/api/core/staging/packages/${packageKey}/nodes/${nodeKey}/archive?force=false`;
mockAxiosDelete(apiUrl);

await new NodeService(testContext).archiveNode(packageKey, nodeKey, false);

expect(loggingTestTransport.logMessages.length).toBe(1);
expect(loggingTestTransport.logMessages[0].message).toContain(`Node ${nodeKey} in package ${packageKey} archived successfully.`);
});

it("Should archive node with force", async () => {
const apiUrl = `https://myTeam.celonis.cloud/pacman/api/core/staging/packages/${packageKey}/nodes/${nodeKey}/archive?force=true`;
mockAxiosDelete(apiUrl);

await new NodeService(testContext).archiveNode(packageKey, nodeKey, true);

expect(loggingTestTransport.logMessages.length).toBe(1);
expect(loggingTestTransport.logMessages[0].message).toContain(`Node ${nodeKey} in package ${packageKey} archived successfully.`);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { NodeTransport, SaveNodeTransport } from "../../../src/commands/configuration-management/interfaces/node.interfaces";
import { mockAxiosPost, mockedPostRequestBodyByUrl } from "../../utls/http-requests-mock";
import { NodeService } from "../../../src/commands/configuration-management/node.service";
import { testContext } from "../../utls/test-context";
import { loggingTestTransport, mockWriteFileSync } from "../../jest.setup";
import { FileService } from "../../../src/core/utils/file-service";
import * as path from "path";

describe("Node create", () => {
const packageKey = "package-key";
const apiUrl = `https://myTeam.celonis.cloud/pacman/api/core/staging/packages/${packageKey}/nodes`;
const validateOnlyUrl = `https://myTeam.celonis.cloud/pacman/api/core/staging/packages/${packageKey}/nodes?validate=true`;

const createdNode: NodeTransport = {
id: "new-node-id",
key: "new-node-key",
name: "New Node",
packageNodeKey: packageKey,
parentNodeKey: packageKey,
packageNodeId: "package-node-id",
type: "VIEW",
invalidContent: false,
creationDate: new Date().toISOString(),
changeDate: new Date().toISOString(),
createdBy: "user-id",
updatedBy: "user-id",
schemaVersion: 2,
flavor: "STUDIO",
};

const saveTransport: SaveNodeTransport = {
key: "new-node-key",
name: "New Node",
parentNodeKey: packageKey,
type: "VIEW",
configuration: { someKey: "someValue" },
};

it("Should create node and print result", async () => {
mockAxiosPost(apiUrl, createdNode);

await new NodeService(testContext).createNode(packageKey, JSON.stringify(saveTransport), false, false);

expect(loggingTestTransport.logMessages.length).toBe(11);
expect(loggingTestTransport.logMessages[0].message).toContain(`ID: ${createdNode.id}`);
expect(loggingTestTransport.logMessages[1].message).toContain(`Key: ${createdNode.key}`);
expect(loggingTestTransport.logMessages[2].message).toContain(`Name: ${createdNode.name}`);
});

it("Should create node and return as JSON", async () => {
mockAxiosPost(apiUrl, createdNode);

await new NodeService(testContext).createNode(packageKey, JSON.stringify(saveTransport), false, true);

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 savedTransport = JSON.parse(mockWriteFileSync.mock.calls[0][1]) as NodeTransport;

expect(savedTransport).toEqual(createdNode);
});

it("Should send correct request body", async () => {
mockAxiosPost(apiUrl, createdNode);

await new NodeService(testContext).createNode(packageKey, JSON.stringify(saveTransport), false, false);

const requestBody = JSON.parse(mockedPostRequestBodyByUrl.get(apiUrl));
expect(requestBody.key).toBe(saveTransport.key);
expect(requestBody.name).toBe(saveTransport.name);
expect(requestBody.parentNodeKey).toBe(saveTransport.parentNodeKey);
expect(requestBody.type).toBe(saveTransport.type);
});

it("Should call validate-only endpoint when validateOnly=true", async () => {
mockAxiosPost(validateOnlyUrl, undefined);

await new NodeService(testContext).createNode(packageKey, JSON.stringify(saveTransport), true, false);

expect(mockWriteFileSync).not.toHaveBeenCalled();
expect(loggingTestTransport.logMessages.length).toBe(1);
expect(loggingTestTransport.logMessages[0].message).toContain(
`Validation successful for node ${saveTransport.key} in package ${packageKey}.`
);
});
});
Loading
Loading