From e82a7cb92a44c2b23a0395e6dbc4743d53cc7956 Mon Sep 17 00:00:00 2001 From: zgjimhaziri Date: Wed, 8 Apr 2026 18:19:42 +0200 Subject: [PATCH] SP-23: Add asset-registry command group for Content CLI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add CLI commands to discover registered asset types from the Pacman Asset Registry public API. Commands: asset-registry list — list all registered asset types asset-registry get --assetType — get the full descriptor for a type Includes-AI-Code: true Made-with: Cursor --- .github/CODEOWNERS | 1 + docs/user-guide/asset-registry-commands.md | 53 +++++++++++ docs/user-guide/index.md | 1 + mkdocs.yaml | 1 + .../asset-registry/asset-registry-api.ts | 28 ++++++ .../asset-registry.interfaces.ts | 35 +++++++ .../asset-registry/asset-registry.service.ts | 71 ++++++++++++++ src/commands/asset-registry/module.ts | 33 +++++++ .../asset-registry/asset-registry-get.spec.ts | 92 +++++++++++++++++++ .../asset-registry-list.spec.ts | 84 +++++++++++++++++ 10 files changed, 399 insertions(+) create mode 100644 docs/user-guide/asset-registry-commands.md create mode 100644 src/commands/asset-registry/asset-registry-api.ts create mode 100644 src/commands/asset-registry/asset-registry.interfaces.ts create mode 100644 src/commands/asset-registry/asset-registry.service.ts create mode 100644 src/commands/asset-registry/module.ts create mode 100644 tests/commands/asset-registry/asset-registry-get.spec.ts create mode 100644 tests/commands/asset-registry/asset-registry-list.spec.ts diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index a76b2ee1..2e6a592b 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -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 diff --git a/docs/user-guide/asset-registry-commands.md b/docs/user-guide/asset-registry-commands.md new file mode 100644 index 00000000..f642d259 --- /dev/null +++ b/docs/user-guide/asset-registry-commands.md @@ -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 ` (required) – The asset type identifier (e.g., `BOARD_V2`, `SEMANTIC_MODEL`) +- `--json` – Write the full response to a JSON file in the working directory diff --git a/docs/user-guide/index.md b/docs/user-guide/index.md index d626e9be..04805f70 100644 --- a/docs/user-guide/index.md +++ b/docs/user-guide/index.md @@ -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 | diff --git a/mkdocs.yaml b/mkdocs.yaml index 9ce624a6..09dd8a81 100644 --- a/mkdocs.yaml +++ b/mkdocs.yaml @@ -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: diff --git a/src/commands/asset-registry/asset-registry-api.ts b/src/commands/asset-registry/asset-registry-api.ts new file mode 100644 index 00000000..12c7a9f9 --- /dev/null +++ b/src/commands/asset-registry/asset-registry-api.ts @@ -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; + + constructor(context: Context) { + this.httpClient = () => context.httpClient; + } + + public async listTypes(): Promise { + 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 { + return this.httpClient() + .get(`/pacman/api/core/asset-registry/types/${encodeURIComponent(assetType)}`) + .catch((e) => { + throw new FatalError(`Problem getting asset type '${assetType}': ${e}`); + }); + } +} diff --git a/src/commands/asset-registry/asset-registry.interfaces.ts b/src/commands/asset-registry/asset-registry.interfaces.ts new file mode 100644 index 00000000..60cf5e7a --- /dev/null +++ b/src/commands/asset-registry/asset-registry.interfaces.ts @@ -0,0 +1,35 @@ +export interface AssetRegistryMetadata { + types: Record; +} + +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[]; +} diff --git a/src/commands/asset-registry/asset-registry.service.ts b/src/commands/asset-registry/asset-registry.service.ts new file mode 100644 index 00000000..a4617f3c --- /dev/null +++ b/src/commands/asset-registry/asset-registry.service.ts @@ -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; + + constructor(context: Context) { + this.api = new AssetRegistryApi(context); + } + + public async listTypes(jsonResponse: boolean): Promise { + 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 { + 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}`); + } + } +} diff --git a/src/commands/asset-registry/module.ts b/src/commands/asset-registry/module.ts new file mode 100644 index 00000000..f8543a2c --- /dev/null +++ b/src/commands/asset-registry/module.ts @@ -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 ", "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 { + await new AssetRegistryService(context).listTypes(!!options.json); + } + + private async getType(context: Context, command: Command, options: OptionValues): Promise { + await new AssetRegistryService(context).getType(options.assetType, !!options.json); + } +} + +export = Module; diff --git a/tests/commands/asset-registry/asset-registry-get.spec.ts b/tests/commands/asset-registry/asset-registry-get.spec.ts new file mode 100644 index 00000000..44610129 --- /dev/null +++ b/tests/commands/asset-registry/asset-registry-get.spec.ts @@ -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"); + }); +}); diff --git a/tests/commands/asset-registry/asset-registry-list.spec.ts b/tests/commands/asset-registry/asset-registry-list.spec.ts new file mode 100644 index 00000000..5fd42944 --- /dev/null +++ b/tests/commands/asset-registry/asset-registry-list.spec.ts @@ -0,0 +1,84 @@ +import { AssetRegistryMetadata } 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 list", () => { + const metadata: AssetRegistryMetadata = { + types: { + BOARD_V2: { + 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: [] }, + }, + SEMANTIC_MODEL: { + assetType: "SEMANTIC_MODEL", + displayName: "Knowledge Model", + description: "Defines KPIs, records, filters, and data bindings for analytics", + group: "DATA_AND_PROCESS_MODELING", + assetSchema: { version: "2.1.0" }, + service: { basePath: "/semantic-layer/api" }, + endpoints: { + schema: "/schema", + validate: "/validate", + methodology: "/methodology", + examples: "/examples", + }, + contributions: { pigEntityTypes: [], dataPipelineEntityTypes: [], actionTypes: [] }, + }, + }, + }; + + it("Should list all asset types", async () => { + mockAxiosGet("https://myTeam.celonis.cloud/pacman/api/core/asset-registry/types", metadata); + + await new AssetRegistryService(testContext).listTypes(false); + + expect(loggingTestTransport.logMessages.length).toBe(2); + expect(loggingTestTransport.logMessages[0].message).toContain("BOARD_V2"); + expect(loggingTestTransport.logMessages[0].message).toContain("View"); + expect(loggingTestTransport.logMessages[0].message).toContain("DASHBOARDS"); + expect(loggingTestTransport.logMessages[1].message).toContain("SEMANTIC_MODEL"); + expect(loggingTestTransport.logMessages[1].message).toContain("Knowledge Model"); + }); + + it("Should list all asset types as JSON", async () => { + mockAxiosGet("https://myTeam.celonis.cloud/pacman/api/core/asset-registry/types", metadata); + + await new AssetRegistryService(testContext).listTypes(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 AssetRegistryMetadata; + expect(Object.keys(written.types).length).toBe(2); + expect(written.types["BOARD_V2"].assetType).toBe("BOARD_V2"); + expect(written.types["SEMANTIC_MODEL"].assetType).toBe("SEMANTIC_MODEL"); + }); + + it("Should handle empty registry", async () => { + mockAxiosGet("https://myTeam.celonis.cloud/pacman/api/core/asset-registry/types", { types: {} }); + + await new AssetRegistryService(testContext).listTypes(false); + + expect(loggingTestTransport.logMessages.length).toBe(1); + expect(loggingTestTransport.logMessages[0].message).toContain("No asset types registered"); + }); +});