Skip to content

Commit 6a0c727

Browse files
authored
SP-23: Add asset-registry command group (#332)
1 parent 9e698a7 commit 6a0c727

File tree

10 files changed

+399
-0
lines changed

10 files changed

+399
-0
lines changed

.github/CODEOWNERS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
/src/core @celonis/astro
66
/src/commands/configuration-management/ @celonis/astro
77
/src/commands/profile/ @celonis/astro
8+
/src/commands/asset-registry/ @celonis/astro
89
/src/commands/deployment/ @celonis/astro
910
/src/commands/action-flows/ @celonis/process-automation
1011
/tests/commands/action-flows/ @celonis/process-automation
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
# Asset Registry Commands
2+
3+
The **asset-registry** command group allows you to discover registered asset types and their service descriptors from the Asset Registry.
4+
This is useful for understanding which asset types are available on the platform, their configuration schema versions, and how to reach their backing services.
5+
6+
## List Asset Types
7+
8+
List all registered asset types and a summary of their metadata.
9+
10+
```
11+
content-cli asset-registry list
12+
```
13+
14+
Example output:
15+
16+
```
17+
BOARD_V2 - View [DASHBOARDS] (basePath: /blueprint/api)
18+
SEMANTIC_MODEL - Knowledge Model [DATA_AND_PROCESS_MODELING] (basePath: /semantic-layer/api)
19+
```
20+
21+
It is also possible to use the `--json` option for writing the full response to a file that gets created in the working directory.
22+
23+
```
24+
content-cli asset-registry list --json
25+
```
26+
27+
## Get Asset Type
28+
29+
Get the full descriptor for a specific asset type, including schema version, service base path, and endpoint paths.
30+
31+
```
32+
content-cli asset-registry get --assetType BOARD_V2
33+
```
34+
35+
Example output:
36+
37+
```
38+
Asset Type: BOARD_V2
39+
Display Name: View
40+
Group: DASHBOARDS
41+
Schema: v2.1.0
42+
Base Path: /blueprint/api
43+
Endpoints:
44+
schema: /schema/board_v2
45+
validate: /validate/board_v2
46+
methodology: /methodology/board_v2
47+
examples: /examples/board_v2
48+
```
49+
50+
Options:
51+
52+
- `--assetType <assetType>` (required) – The asset type identifier (e.g., `BOARD_V2`, `SEMANTIC_MODEL`)
53+
- `--json` – Write the full response to a JSON file in the working directory

docs/user-guide/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,6 @@ Content CLI organizes its commands into groups by area. Each group covers a spec
77
| [Studio Commands](./studio-commands.md) | Pull and push packages, assets, spaces, and widgets to and from Studio |
88
| [Config Commands](./config-commands.md) | List, batch export, and import all packages and their configurations |
99
| [Deployment Commands](./deployment-commands.md) | Create deployments, list history, check active deployments, and manage targets |
10+
| [Asset Registry Commands](./asset-registry-commands.md) | Discover registered asset types and their service descriptors |
1011
| [Data Pool Commands](./data-pool-commands.md) | Export and import Data Pools with their dependencies |
1112
| [Action Flow Commands](./action-flow-commands.md) | Analyze and export/import Action Flows and their dependencies |

mkdocs.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ nav:
1414
- Studio Commands: './user-guide/studio-commands.md'
1515
- Config Commands: './user-guide/config-commands.md'
1616
- Deployment Commands: './user-guide/deployment-commands.md'
17+
- Asset Registry Commands: './user-guide/asset-registry-commands.md'
1718
- Data Pool Commands: './user-guide/data-pool-commands.md'
1819
- Action Flow Commands: './user-guide/action-flow-commands.md'
1920
- Development:
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { HttpClient } from "../../core/http/http-client";
2+
import { Context } from "../../core/command/cli-context";
3+
import { AssetRegistryDescriptor, AssetRegistryMetadata } from "./asset-registry.interfaces";
4+
import { FatalError } from "../../core/utils/logger";
5+
6+
export class AssetRegistryApi {
7+
private httpClient: () => HttpClient;
8+
9+
constructor(context: Context) {
10+
this.httpClient = () => context.httpClient;
11+
}
12+
13+
public async listTypes(): Promise<AssetRegistryMetadata> {
14+
return this.httpClient()
15+
.get("/pacman/api/core/asset-registry/types")
16+
.catch((e) => {
17+
throw new FatalError(`Problem listing asset registry types: ${e}`);
18+
});
19+
}
20+
21+
public async getType(assetType: string): Promise<AssetRegistryDescriptor> {
22+
return this.httpClient()
23+
.get(`/pacman/api/core/asset-registry/types/${encodeURIComponent(assetType)}`)
24+
.catch((e) => {
25+
throw new FatalError(`Problem getting asset type '${assetType}': ${e}`);
26+
});
27+
}
28+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
export interface AssetRegistryMetadata {
2+
types: Record<string, AssetRegistryDescriptor>;
3+
}
4+
5+
export interface AssetRegistryDescriptor {
6+
assetType: string;
7+
displayName: string;
8+
description: string | null;
9+
group: string;
10+
assetSchema: AssetSchema;
11+
service: AssetService;
12+
endpoints: AssetEndpoints;
13+
contributions: AssetContributions;
14+
}
15+
16+
export interface AssetSchema {
17+
version: string;
18+
}
19+
20+
export interface AssetService {
21+
basePath: string;
22+
}
23+
24+
export interface AssetEndpoints {
25+
schema: string;
26+
validate: string;
27+
methodology?: string;
28+
examples?: string;
29+
}
30+
31+
export interface AssetContributions {
32+
pigEntityTypes: string[];
33+
dataPipelineEntityTypes: string[];
34+
actionTypes: string[];
35+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import { AssetRegistryApi } from "./asset-registry-api";
2+
import { AssetRegistryDescriptor } from "./asset-registry.interfaces";
3+
import { Context } from "../../core/command/cli-context";
4+
import { fileService, FileService } from "../../core/utils/file-service";
5+
import { logger } from "../../core/utils/logger";
6+
import { v4 as uuidv4 } from "uuid";
7+
8+
export class AssetRegistryService {
9+
private api: AssetRegistryApi;
10+
11+
constructor(context: Context) {
12+
this.api = new AssetRegistryApi(context);
13+
}
14+
15+
public async listTypes(jsonResponse: boolean): Promise<void> {
16+
const metadata = await this.api.listTypes();
17+
const descriptors = Object.values(metadata.types);
18+
19+
if (jsonResponse) {
20+
const filename = uuidv4() + ".json";
21+
fileService.writeToFileWithGivenName(JSON.stringify(metadata), filename);
22+
logger.info(FileService.fileDownloadedMessage + filename);
23+
} else {
24+
if (descriptors.length === 0) {
25+
logger.info("No asset types registered.");
26+
return;
27+
}
28+
descriptors.forEach((descriptor) => {
29+
this.logDescriptorSummary(descriptor);
30+
});
31+
}
32+
}
33+
34+
public async getType(assetType: string, jsonResponse: boolean): Promise<void> {
35+
const descriptor = await this.api.getType(assetType);
36+
37+
if (jsonResponse) {
38+
const filename = uuidv4() + ".json";
39+
fileService.writeToFileWithGivenName(JSON.stringify(descriptor), filename);
40+
logger.info(FileService.fileDownloadedMessage + filename);
41+
} else {
42+
this.logDescriptorDetail(descriptor);
43+
}
44+
}
45+
46+
private logDescriptorSummary(descriptor: AssetRegistryDescriptor): void {
47+
logger.info(
48+
`${descriptor.assetType} - ${descriptor.displayName} [${descriptor.group}] (basePath: ${descriptor.service.basePath})`
49+
);
50+
}
51+
52+
private logDescriptorDetail(descriptor: AssetRegistryDescriptor): void {
53+
logger.info(`Asset Type: ${descriptor.assetType}`);
54+
logger.info(`Display Name: ${descriptor.displayName}`);
55+
if (descriptor.description) {
56+
logger.info(`Description: ${descriptor.description}`);
57+
}
58+
logger.info(`Group: ${descriptor.group}`);
59+
logger.info(`Schema: v${descriptor.assetSchema.version}`);
60+
logger.info(`Base Path: ${descriptor.service.basePath}`);
61+
logger.info(`Endpoints:`);
62+
logger.info(` schema: ${descriptor.endpoints.schema}`);
63+
logger.info(` validate: ${descriptor.endpoints.validate}`);
64+
if (descriptor.endpoints.methodology) {
65+
logger.info(` methodology: ${descriptor.endpoints.methodology}`);
66+
}
67+
if (descriptor.endpoints.examples) {
68+
logger.info(` examples: ${descriptor.endpoints.examples}`);
69+
}
70+
}
71+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { Configurator, IModule } from "../../core/command/module-handler";
2+
import { Context } from "../../core/command/cli-context";
3+
import { Command, OptionValues } from "commander";
4+
import { AssetRegistryService } from "./asset-registry.service";
5+
6+
class Module extends IModule {
7+
8+
public register(context: Context, configurator: Configurator): void {
9+
const assetRegistryCommand = configurator.command("asset-registry")
10+
.description("Manage the asset registry — discover registered asset types and their service descriptors.");
11+
12+
assetRegistryCommand.command("list")
13+
.description("List all registered asset types")
14+
.option("--json", "Return the response as a JSON file")
15+
.action(this.listTypes);
16+
17+
assetRegistryCommand.command("get")
18+
.description("Get the descriptor for a specific asset type")
19+
.requiredOption("--assetType <assetType>", "The asset type identifier (e.g., BOARD_V2)")
20+
.option("--json", "Return the response as a JSON file")
21+
.action(this.getType);
22+
}
23+
24+
private async listTypes(context: Context, command: Command, options: OptionValues): Promise<void> {
25+
await new AssetRegistryService(context).listTypes(!!options.json);
26+
}
27+
28+
private async getType(context: Context, command: Command, options: OptionValues): Promise<void> {
29+
await new AssetRegistryService(context).getType(options.assetType, !!options.json);
30+
}
31+
}
32+
33+
export = Module;
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import { AssetRegistryDescriptor } from "../../../src/commands/asset-registry/asset-registry.interfaces";
2+
import { mockAxiosGet } from "../../utls/http-requests-mock";
3+
import { AssetRegistryService } from "../../../src/commands/asset-registry/asset-registry.service";
4+
import { testContext } from "../../utls/test-context";
5+
import { loggingTestTransport, mockWriteFileSync } from "../../jest.setup";
6+
import { FileService } from "../../../src/core/utils/file-service";
7+
import * as path from "path";
8+
9+
describe("Asset registry get", () => {
10+
const boardDescriptor: AssetRegistryDescriptor = {
11+
assetType: "BOARD_V2",
12+
displayName: "View",
13+
description: null,
14+
group: "DASHBOARDS",
15+
assetSchema: { version: "2.1.0" },
16+
service: { basePath: "/blueprint/api" },
17+
endpoints: {
18+
schema: "/schema/board_v2",
19+
validate: "/validate/board_v2",
20+
methodology: "/methodology/board_v2",
21+
examples: "/examples/board_v2",
22+
},
23+
contributions: { pigEntityTypes: [], dataPipelineEntityTypes: [], actionTypes: [] },
24+
};
25+
26+
it("Should get a specific asset type", async () => {
27+
mockAxiosGet("https://myTeam.celonis.cloud/pacman/api/core/asset-registry/types/BOARD_V2", boardDescriptor);
28+
29+
await new AssetRegistryService(testContext).getType("BOARD_V2", false);
30+
31+
const messages = loggingTestTransport.logMessages.map((m) => m.message);
32+
expect(messages).toEqual(
33+
expect.arrayContaining([
34+
expect.stringContaining("BOARD_V2"),
35+
expect.stringContaining("View"),
36+
expect.stringContaining("DASHBOARDS"),
37+
expect.stringContaining("/blueprint/api"),
38+
expect.stringContaining("/schema/board_v2"),
39+
expect.stringContaining("/validate/board_v2"),
40+
])
41+
);
42+
});
43+
44+
it("Should get a specific asset type as JSON", async () => {
45+
mockAxiosGet("https://myTeam.celonis.cloud/pacman/api/core/asset-registry/types/BOARD_V2", boardDescriptor);
46+
47+
await new AssetRegistryService(testContext).getType("BOARD_V2", true);
48+
49+
const expectedFileName = loggingTestTransport.logMessages[0].message.split(FileService.fileDownloadedMessage)[1];
50+
expect(mockWriteFileSync).toHaveBeenCalledWith(
51+
path.resolve(process.cwd(), expectedFileName),
52+
expect.any(String),
53+
{ encoding: "utf-8" }
54+
);
55+
56+
const written = JSON.parse(mockWriteFileSync.mock.calls[0][1]) as AssetRegistryDescriptor;
57+
expect(written.assetType).toBe("BOARD_V2");
58+
expect(written.displayName).toBe("View");
59+
expect(written.service.basePath).toBe("/blueprint/api");
60+
});
61+
62+
it("Should include optional endpoints when present", async () => {
63+
mockAxiosGet("https://myTeam.celonis.cloud/pacman/api/core/asset-registry/types/BOARD_V2", boardDescriptor);
64+
65+
await new AssetRegistryService(testContext).getType("BOARD_V2", false);
66+
67+
const messages = loggingTestTransport.logMessages.map((m) => m.message);
68+
expect(messages).toEqual(
69+
expect.arrayContaining([
70+
expect.stringContaining("/methodology/board_v2"),
71+
expect.stringContaining("/examples/board_v2"),
72+
])
73+
);
74+
});
75+
76+
it("Should omit optional endpoints when absent", async () => {
77+
const descriptorWithoutOptionals: AssetRegistryDescriptor = {
78+
...boardDescriptor,
79+
endpoints: {
80+
schema: "/schema/board_v2",
81+
validate: "/validate/board_v2",
82+
},
83+
};
84+
mockAxiosGet("https://myTeam.celonis.cloud/pacman/api/core/asset-registry/types/BOARD_V2", descriptorWithoutOptionals);
85+
86+
await new AssetRegistryService(testContext).getType("BOARD_V2", false);
87+
88+
const messages = loggingTestTransport.logMessages.map((m) => m.message).join("\n");
89+
expect(messages).not.toContain("methodology");
90+
expect(messages).not.toContain("examples");
91+
});
92+
});

0 commit comments

Comments
 (0)