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
1 change: 1 addition & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
/src/core @celonis/astro
/src/commands/configuration-management/ @celonis/astro
/src/commands/profile/ @celonis/astro
/src/commands/asset-registry/ @celonis/astro
/src/commands/deployment/ @celonis/astro
/src/commands/action-flows/ @celonis/process-automation
/tests/commands/action-flows/ @celonis/process-automation
Expand Down
53 changes: 53 additions & 0 deletions docs/user-guide/asset-registry-commands.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# Asset Registry Commands

The **asset-registry** command group allows you to discover registered asset types and their service descriptors from the Asset Registry.
This is useful for understanding which asset types are available on the platform, their configuration schema versions, and how to reach their backing services.

## List Asset Types

List all registered asset types and a summary of their metadata.

```
content-cli asset-registry list
```

Example output:

```
BOARD_V2 - View [DASHBOARDS] (basePath: /blueprint/api)
SEMANTIC_MODEL - Knowledge Model [DATA_AND_PROCESS_MODELING] (basePath: /semantic-layer/api)
```

It is also possible to use the `--json` option for writing the full response to a file that gets created in the working directory.

```
content-cli asset-registry list --json
```

## Get Asset Type

Get the full descriptor for a specific asset type, including schema version, service base path, and endpoint paths.

```
content-cli asset-registry get --assetType BOARD_V2
```

Example output:

```
Asset Type: BOARD_V2
Display Name: View
Group: DASHBOARDS
Schema: v2.1.0
Base Path: /blueprint/api
Endpoints:
schema: /schema/board_v2
validate: /validate/board_v2
methodology: /methodology/board_v2
examples: /examples/board_v2
```

Options:

- `--assetType <assetType>` (required) – The asset type identifier (e.g., `BOARD_V2`, `SEMANTIC_MODEL`)
- `--json` – Write the full response to a JSON file in the working directory
1 change: 1 addition & 0 deletions docs/user-guide/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,6 @@ Content CLI organizes its commands into groups by area. Each group covers a spec
| [Studio Commands](./studio-commands.md) | Pull and push packages, assets, spaces, and widgets to and from Studio |
| [Config Commands](./config-commands.md) | List, batch export, and import all packages and their configurations |
| [Deployment Commands](./deployment-commands.md) | Create deployments, list history, check active deployments, and manage targets |
| [Asset Registry Commands](./asset-registry-commands.md) | Discover registered asset types and their service descriptors |
| [Data Pool Commands](./data-pool-commands.md) | Export and import Data Pools with their dependencies |
| [Action Flow Commands](./action-flow-commands.md) | Analyze and export/import Action Flows and their dependencies |
1 change: 1 addition & 0 deletions mkdocs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ nav:
- Studio Commands: './user-guide/studio-commands.md'
- Config Commands: './user-guide/config-commands.md'
- Deployment Commands: './user-guide/deployment-commands.md'
- Asset Registry Commands: './user-guide/asset-registry-commands.md'
- Data Pool Commands: './user-guide/data-pool-commands.md'
- Action Flow Commands: './user-guide/action-flow-commands.md'
- Development:
Expand Down
28 changes: 28 additions & 0 deletions src/commands/asset-registry/asset-registry-api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { HttpClient } from "../../core/http/http-client";
import { Context } from "../../core/command/cli-context";
import { AssetRegistryDescriptor, AssetRegistryMetadata } from "./asset-registry.interfaces";
import { FatalError } from "../../core/utils/logger";

export class AssetRegistryApi {
private httpClient: () => HttpClient;

Check warning on line 7 in src/commands/asset-registry/asset-registry-api.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Member 'httpClient' is never reassigned; mark it as `readonly`.

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

constructor(context: Context) {
this.httpClient = () => context.httpClient;
}

public async listTypes(): Promise<AssetRegistryMetadata> {
return this.httpClient()
.get("/pacman/api/core/asset-registry/types")
.catch((e) => {
throw new FatalError(`Problem listing asset registry types: ${e}`);
});
}

public async getType(assetType: string): Promise<AssetRegistryDescriptor> {
return this.httpClient()
.get(`/pacman/api/core/asset-registry/types/${encodeURIComponent(assetType)}`)
.catch((e) => {
throw new FatalError(`Problem getting asset type '${assetType}': ${e}`);
});
}
}
35 changes: 35 additions & 0 deletions src/commands/asset-registry/asset-registry.interfaces.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
export interface AssetRegistryMetadata {
types: Record<string, AssetRegistryDescriptor>;
}

export interface AssetRegistryDescriptor {
assetType: string;
displayName: string;
description: string | null;
group: string;
assetSchema: AssetSchema;
service: AssetService;
endpoints: AssetEndpoints;
contributions: AssetContributions;
}

export interface AssetSchema {
version: string;
}

export interface AssetService {
basePath: string;
}

export interface AssetEndpoints {
schema: string;
validate: string;
methodology?: string;
examples?: string;
}

export interface AssetContributions {
pigEntityTypes: string[];
dataPipelineEntityTypes: string[];
actionTypes: string[];
}
71 changes: 71 additions & 0 deletions src/commands/asset-registry/asset-registry.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { AssetRegistryApi } from "./asset-registry-api";
import { AssetRegistryDescriptor } from "./asset-registry.interfaces";
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";

export class AssetRegistryService {
private api: AssetRegistryApi;

Check warning on line 9 in src/commands/asset-registry/asset-registry.service.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Member 'api' is never reassigned; mark it as `readonly`.

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

constructor(context: Context) {
this.api = new AssetRegistryApi(context);
}

public async listTypes(jsonResponse: boolean): Promise<void> {
const metadata = await this.api.listTypes();
const descriptors = Object.values(metadata.types);

if (jsonResponse) {
const filename = uuidv4() + ".json";
fileService.writeToFileWithGivenName(JSON.stringify(metadata), filename);
logger.info(FileService.fileDownloadedMessage + filename);
} else {
if (descriptors.length === 0) {
logger.info("No asset types registered.");
return;
}
descriptors.forEach((descriptor) => {
this.logDescriptorSummary(descriptor);
});
}
}

public async getType(assetType: string, jsonResponse: boolean): Promise<void> {
const descriptor = await this.api.getType(assetType);

if (jsonResponse) {
const filename = uuidv4() + ".json";
fileService.writeToFileWithGivenName(JSON.stringify(descriptor), filename);
logger.info(FileService.fileDownloadedMessage + filename);
} else {
this.logDescriptorDetail(descriptor);
}
}

private logDescriptorSummary(descriptor: AssetRegistryDescriptor): void {
logger.info(
`${descriptor.assetType} - ${descriptor.displayName} [${descriptor.group}] (basePath: ${descriptor.service.basePath})`
);
}

private logDescriptorDetail(descriptor: AssetRegistryDescriptor): void {
logger.info(`Asset Type: ${descriptor.assetType}`);
logger.info(`Display Name: ${descriptor.displayName}`);
if (descriptor.description) {
logger.info(`Description: ${descriptor.description}`);
}
logger.info(`Group: ${descriptor.group}`);
logger.info(`Schema: v${descriptor.assetSchema.version}`);
logger.info(`Base Path: ${descriptor.service.basePath}`);
logger.info(`Endpoints:`);
logger.info(` schema: ${descriptor.endpoints.schema}`);
logger.info(` validate: ${descriptor.endpoints.validate}`);
if (descriptor.endpoints.methodology) {
logger.info(` methodology: ${descriptor.endpoints.methodology}`);
}
if (descriptor.endpoints.examples) {
logger.info(` examples: ${descriptor.endpoints.examples}`);
}
}
}
33 changes: 33 additions & 0 deletions src/commands/asset-registry/module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { Configurator, IModule } from "../../core/command/module-handler";
import { Context } from "../../core/command/cli-context";
import { Command, OptionValues } from "commander";
import { AssetRegistryService } from "./asset-registry.service";

class Module extends IModule {

public register(context: Context, configurator: Configurator): void {
const assetRegistryCommand = configurator.command("asset-registry")
.description("Manage the asset registry — discover registered asset types and their service descriptors.");

assetRegistryCommand.command("list")
.description("List all registered asset types")
.option("--json", "Return the response as a JSON file")
.action(this.listTypes);

assetRegistryCommand.command("get")
.description("Get the descriptor for a specific asset type")
.requiredOption("--assetType <assetType>", "The asset type identifier (e.g., BOARD_V2)")
.option("--json", "Return the response as a JSON file")
.action(this.getType);
}

private async listTypes(context: Context, command: Command, options: OptionValues): Promise<void> {
await new AssetRegistryService(context).listTypes(!!options.json);
}

private async getType(context: Context, command: Command, options: OptionValues): Promise<void> {
await new AssetRegistryService(context).getType(options.assetType, !!options.json);
}
}

export = Module;
92 changes: 92 additions & 0 deletions tests/commands/asset-registry/asset-registry-get.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { AssetRegistryDescriptor } from "../../../src/commands/asset-registry/asset-registry.interfaces";
import { mockAxiosGet } from "../../utls/http-requests-mock";
import { AssetRegistryService } from "../../../src/commands/asset-registry/asset-registry.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("Asset registry get", () => {
const boardDescriptor: AssetRegistryDescriptor = {
assetType: "BOARD_V2",
displayName: "View",
description: null,
group: "DASHBOARDS",
assetSchema: { version: "2.1.0" },
service: { basePath: "/blueprint/api" },
endpoints: {
schema: "/schema/board_v2",
validate: "/validate/board_v2",
methodology: "/methodology/board_v2",
examples: "/examples/board_v2",
},
contributions: { pigEntityTypes: [], dataPipelineEntityTypes: [], actionTypes: [] },
};

it("Should get a specific asset type", async () => {
mockAxiosGet("https://myTeam.celonis.cloud/pacman/api/core/asset-registry/types/BOARD_V2", boardDescriptor);

await new AssetRegistryService(testContext).getType("BOARD_V2", false);

const messages = loggingTestTransport.logMessages.map((m) => m.message);
expect(messages).toEqual(
expect.arrayContaining([
expect.stringContaining("BOARD_V2"),
expect.stringContaining("View"),
expect.stringContaining("DASHBOARDS"),
expect.stringContaining("/blueprint/api"),
expect.stringContaining("/schema/board_v2"),
expect.stringContaining("/validate/board_v2"),
])
);
});

it("Should get a specific asset type as JSON", async () => {
mockAxiosGet("https://myTeam.celonis.cloud/pacman/api/core/asset-registry/types/BOARD_V2", boardDescriptor);

await new AssetRegistryService(testContext).getType("BOARD_V2", 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 written = JSON.parse(mockWriteFileSync.mock.calls[0][1]) as AssetRegistryDescriptor;
expect(written.assetType).toBe("BOARD_V2");
expect(written.displayName).toBe("View");
expect(written.service.basePath).toBe("/blueprint/api");
});

it("Should include optional endpoints when present", async () => {
mockAxiosGet("https://myTeam.celonis.cloud/pacman/api/core/asset-registry/types/BOARD_V2", boardDescriptor);

await new AssetRegistryService(testContext).getType("BOARD_V2", false);

const messages = loggingTestTransport.logMessages.map((m) => m.message);
expect(messages).toEqual(
expect.arrayContaining([
expect.stringContaining("/methodology/board_v2"),
expect.stringContaining("/examples/board_v2"),
])
);
});

it("Should omit optional endpoints when absent", async () => {
const descriptorWithoutOptionals: AssetRegistryDescriptor = {
...boardDescriptor,
endpoints: {
schema: "/schema/board_v2",
validate: "/validate/board_v2",
},
};
mockAxiosGet("https://myTeam.celonis.cloud/pacman/api/core/asset-registry/types/BOARD_V2", descriptorWithoutOptionals);

await new AssetRegistryService(testContext).getType("BOARD_V2", false);

const messages = loggingTestTransport.logMessages.map((m) => m.message).join("\n");
expect(messages).not.toContain("methodology");
expect(messages).not.toContain("examples");
});
});
Loading
Loading