Skip to content
Merged
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
46 changes: 39 additions & 7 deletions .cursor/skills/asset-registry-endpoints/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -223,20 +223,49 @@ methodology — a 404 means the endpoint is not available.
$CLI asset-registry methodology --assetType <ASSET_TYPE> -p <profile>
```

### Validate (POST — via config import)
### Validate

Use `config import --validate` to validate assets against their schema before
importing:
Two top-level modes:

**Build-from-options** — `--packageKey` plus exactly one of `--nodeKey` (stored
node) or `--configuration` (raw configuration JSON). `--nodeKey` and
`--configuration` are mutually exclusive.

Validate an already-stored node:

```bash
$CLI asset-registry validate --assetType <ASSET_TYPE> \
--packageKey <pkg> --nodeKey <key> -p <profile>
```

Validate a raw configuration before import:

```bash
$CLI asset-registry validate --assetType <ASSET_TYPE> \
--packageKey <pkg> \
--configuration '<configuration-json>' -p <profile>
```

**`-f` / `--file` mode** — Provide a JSON file containing a full
`ValidateRequest` body. Use this for multi-node validation or any case the
build-from-options mode doesn't cover. Mutually exclusive with the
build-from-options flags.

```bash
$CLI asset-registry validate --assetType <ASSET_TYPE> -f request.json -p <profile>
```

You can also validate during import with `config import --validate`:

```bash
$CLI config import -d <export_dir> --validate --overwrite -p <profile>
```

**Important**: If validation returns errors, do **not** proceed with the import.
Instead, fix the schema violations in the node JSON and re-run the command. If
you cannot resolve the errors automatically, present the validation results to
the user and ask whether they want to continue importing with invalid
configuration or stop to fix it manually.
Instead, fix the schema violations in the node JSON and re-validate. If you
cannot resolve the errors automatically, present the validation results to the
user and ask whether they want to continue importing with invalid configuration
or stop to fix it manually.

## Troubleshooting

Expand Down Expand Up @@ -284,6 +313,9 @@ $CLI config import -d <export_dir> --validate --overwrite -p <profile>
| `asset-registry list` | List all registered asset types |
| `asset-registry get --assetType X` | Get the full descriptor for an asset type |
| `asset-registry schema --assetType X` | Get the JSON Schema for the asset's configuration |
| `asset-registry validate --assetType X --packageKey P --nodeKey K` | Validate an already-stored node |
| `asset-registry validate --assetType X --packageKey P --configuration '{}'` | Validate a raw configuration before import |
| `asset-registry validate --assetType X -f request.json` | Validate using a full ValidateRequest file (multi-node, etc.) |
| `asset-registry examples --assetType X` | Get example configurations (if available) |
| `asset-registry methodology --assetType X` | Get methodology / best-practices (if available) |
| `config list` | List packages |
Expand Down
21 changes: 18 additions & 3 deletions docs/user-guide/agentic-development-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,15 +73,30 @@ Add a new JSON file in the `nodes/` directory:

Set `schemaVersion` to the value from the asset descriptor's `assetSchema.version` field (returned by `asset-registry get`). The `spaceId` is required — omitting it causes import errors.

### 5. Validate and import
### 5. Validate

Before importing, validate the asset configuration:

```bash
content-cli asset-registry validate --assetType <ASSET_TYPE> \
--packageKey <package-key> --configuration '{ ... }'
```

Or validate during import with the `--validate` flag:

```bash
content-cli config import -d <export_dir> --validate --overwrite
```

The `--validate` option performs schema validations for the assets. If there are no schema validations, then the package and its assets are imported. Otherwise, the validation errors are returned and the package import isn't performed.
If validation returns errors, fix the issues before importing.

### 6. Import

```bash
content-cli config import -d <export_dir> --overwrite
```

This creates a new version in staging (not deployed) if there are no schema validation errors. To create a brand-new package instead of updating, omit `--overwrite`.
This creates a new version in staging (not deployed). To create a brand-new package instead of updating, omit `--overwrite`.

To later export a staging version, use `--keysByVersion`:

Expand Down
64 changes: 64 additions & 0 deletions docs/user-guide/asset-registry-commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,70 @@ Options:
- `--assetType <assetType>` (required) – The asset type identifier
- `--json` – Write the schema to a JSON file in the working directory

## Validate

Validate asset configurations against the asset service's validation endpoint. There are two top-level modes:

1. **Build-from-options mode** – `--packageKey` plus exactly one of:
- **`--nodeKey`** to validate an already-stored node on the platform, or
- **`--configuration`** to validate a raw configuration JSON before import.

`--nodeKey` and `--configuration` are mutually exclusive. The CLI wraps the inputs into a `ValidateRequest` envelope for you.

2. **File mode (`-f` / `--file`)** – Provide a JSON file containing the full `ValidateRequest` body. Use this for multi-node validation or any case the build-from-options mode doesn't cover. Mutually exclusive with `--packageKey`, `--nodeKey` and `--configuration`.

### Validate an already-stored node (`--nodeKey`)

```
content-cli asset-registry validate --assetType BOARD_V2 \
--packageKey my-pkg --nodeKey my-view
```

Sends:

```json
{
"assetType": "BOARD_V2",
"packageKey": "my-pkg",
"nodeKeys": ["my-view"]
}
```

### Validate a raw configuration (`--configuration`)

```
content-cli asset-registry validate --assetType BOARD_V2 \
--packageKey my-pkg \
--configuration '{"components":[{"type":"kpi"}]}'
```

Sends:

```json
{
"assetType": "BOARD_V2",
"packageKey": "my-pkg",
"nodes": [{ "key": "validation-node", "configuration": { "components": [{ "type": "kpi" }] } }]
}
```

### Full request from file (`-f`)

```
content-cli asset-registry validate --assetType BOARD_V2 -f request.json
```

Use this when you need control over the full body (e.g., multiple inline nodes with specific keys).

### Options

- `--assetType <assetType>` (required) – The asset type identifier
- `--packageKey <packageKey>` – Package key. Required when validating with `--nodeKey` or `--configuration`.
- `--nodeKey <nodeKey>` – Key of an already-stored node to validate (use with `--packageKey`).
- `--configuration <configuration>` – Inline JSON of a configuration to validate (use with `--packageKey`).
- `-f, --file <file>` – Path to a JSON file containing a full `ValidateRequest` body. Mutually exclusive with the build-from-options flags.
- `--json` – Write the validation response to a JSON file in the working directory

## Get Examples

Fetch example configurations for an asset type. Not all asset types provide examples.
Expand Down
8 changes: 8 additions & 0 deletions src/commands/asset-registry/asset-registry-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,4 +49,12 @@ export class AssetRegistryApi {
throw new FatalError(`Problem getting methodology for asset type '${assetType}': ${e}`);
});
}

public async validate(assetType: string, body: any): Promise<any> {
return this.httpClient()
.post(`/pacman/api/core/asset-registry/validate/${encodeURIComponent(assetType)}`, body)
.catch((e) => {
throw new FatalError(`Problem validating asset type '${assetType}': ${e}`);
});
}
}
9 changes: 9 additions & 0 deletions src/commands/asset-registry/asset-registry.interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,12 @@ export interface AssetContributions {
dataPipelineEntityTypes: string[];
actionTypes: string[];
}

export interface ValidateOptions {
assetType: string;
packageKey?: string;
nodeKey?: string;
configuration?: string;
file?: string;
json: boolean;
}
66 changes: 64 additions & 2 deletions src/commands/asset-registry/asset-registry.service.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { AssetRegistryApi } from "./asset-registry-api";
import { AssetRegistryDescriptor } from "./asset-registry.interfaces";
import { AssetRegistryDescriptor, ValidateOptions } 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 { FatalError, logger } from "../../core/utils/logger";
import { v4 as uuidv4 } from "uuid";
import * as fs from "fs";

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

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `node:fs` over `fs`.

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

export class AssetRegistryService {
private api: AssetRegistryApi;
Expand Down Expand Up @@ -58,6 +59,67 @@
this.outputResponse(data, jsonResponse);
}

public async validate(opts: ValidateOptions): Promise<void> {
const payload = this.buildValidatePayload(opts);
const data = await this.api.validate(opts.assetType, payload);
this.outputResponse(data, opts.json);
}

private static readonly INLINE_VALIDATION_NODE_KEY = "validation-node";

private buildValidatePayload(opts: ValidateOptions): any {
const hasNodeKey = !!opts.nodeKey;
const hasConfig = !!opts.configuration;
const hasFile = !!opts.file;

if (hasFile && (hasNodeKey || hasConfig || !!opts.packageKey)) {
throw new FatalError(
"Option -f is mutually exclusive with --packageKey, --nodeKey and --configuration."
);
}

if (hasFile) {
return this.parseJson(fs.readFileSync(opts.file!, "utf-8"), `-f ${opts.file}`);

Check warning on line 82 in src/commands/asset-registry/asset-registry.service.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=AZ2bp_hFC5IK9t0X7tON&open=AZ2bp_hFC5IK9t0X7tON&pullRequest=342
}

if (hasNodeKey && hasConfig) {
throw new FatalError(
"Options --nodeKey and --configuration are mutually exclusive. Use --nodeKey to validate a stored node, or --configuration to validate a configuration. For full control, use -f."
);
}
if (!hasNodeKey && !hasConfig) {
throw new FatalError(
"Provide --packageKey with one of --nodeKey (validate a stored node) or --configuration (validate a configuration), or use -f for a full request file."
);
}
if (!opts.packageKey) {
throw new FatalError("--packageKey is required when using --nodeKey or --configuration.");
}

if (hasNodeKey) {
return {
assetType: opts.assetType,
packageKey: opts.packageKey,
nodeKeys: [opts.nodeKey],
};
}

const configJson = this.parseJson(opts.configuration!, "--configuration");

Check warning on line 107 in src/commands/asset-registry/asset-registry.service.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=AZ3PfJA9oEgnIIUbVzzA&open=AZ3PfJA9oEgnIIUbVzzA&pullRequest=342
return {
assetType: opts.assetType,
packageKey: opts.packageKey,
nodes: [{ key: AssetRegistryService.INLINE_VALIDATION_NODE_KEY, configuration: configJson }],
};
}

private parseJson(raw: string, source: string): any {
try {
return JSON.parse(raw);
} catch {
throw new FatalError(`Invalid JSON in ${source}.`);
}
}

private outputResponse(data: any, jsonResponse: boolean): void {
if (jsonResponse) {
const filename = uuidv4() + ".json";
Expand Down
21 changes: 21 additions & 0 deletions src/commands/asset-registry/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,16 @@ class Module extends IModule {
.option("--json", "Return the response as a JSON file")
.action(this.getExamples);

assetRegistryCommand.command("validate")
.description("Validate asset configuration against the asset service's validate endpoint.")
.requiredOption("--assetType <assetType>", "The asset type identifier (e.g., BOARD_V2)")
.option("--packageKey <packageKey>", "Package key. Required when validating with --nodeKey or --configuration.")
.option("--nodeKey <nodeKey>", "Key of an already-stored node to validate (use with --packageKey).")
.option("--configuration <configuration>", "Inline JSON of a configuration to validate (use with --packageKey).")
.option("-f, --file <file>", "Path to a JSON file containing a full ValidateRequest body. Mutually exclusive with the build-from-options flags.")
.option("--json", "Return the response as a JSON file")
.action(this.validate);

assetRegistryCommand.command("methodology")
.description("Get the methodology / best-practices guide for an asset type")
.requiredOption("--assetType <assetType>", "The asset type identifier (e.g., BOARD_V2)")
Expand All @@ -51,6 +61,17 @@ class Module extends IModule {
await new AssetRegistryService(context).getSchema(options.assetType, !!options.json);
}

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

private async getExamples(context: Context, command: Command, options: OptionValues): Promise<void> {
await new AssetRegistryService(context).getExamples(options.assetType, !!options.json);
}
Expand Down
54 changes: 54 additions & 0 deletions tests/commands/asset-registry/asset-registry-module.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ describe("Asset Registry Module", () => {
listTypes: jest.fn().mockResolvedValue(undefined),
getType: jest.fn().mockResolvedValue(undefined),
getSchema: jest.fn().mockResolvedValue(undefined),
validate: jest.fn().mockResolvedValue(undefined),
getExamples: jest.fn().mockResolvedValue(undefined),
getMethodology: jest.fn().mockResolvedValue(undefined),
} as any;
Expand All @@ -33,6 +34,59 @@ describe("Asset Registry Module", () => {
expect(mockService.getSchema).toHaveBeenCalledWith("BOARD_V2", true);
});

it("should call validate with --configuration sub-mode options", async () => {
const options: OptionValues = {
assetType: "BOARD_V2",
packageKey: "my-pkg",
configuration: '{"components":[]}',
json: true,
};
await (module as any).validate(testContext, mockCommand, options);
expect(mockService.validate).toHaveBeenCalledWith({
assetType: "BOARD_V2",
packageKey: "my-pkg",
nodeKey: undefined,
configuration: '{"components":[]}',
file: undefined,
json: true,
});
});

it("should call validate with --nodeKey sub-mode options", async () => {
const options: OptionValues = {
assetType: "BOARD_V2",
packageKey: "my-pkg",
nodeKey: "my-view",
json: "",
};
await (module as any).validate(testContext, mockCommand, options);
expect(mockService.validate).toHaveBeenCalledWith({
assetType: "BOARD_V2",
packageKey: "my-pkg",
nodeKey: "my-view",
configuration: undefined,
file: undefined,
json: false,
});
});

it("should call validate with file mode options", async () => {
const options: OptionValues = {
assetType: "BOARD_V2",
file: "request.json",
json: "",
};
await (module as any).validate(testContext, mockCommand, options);
expect(mockService.validate).toHaveBeenCalledWith({
assetType: "BOARD_V2",
packageKey: undefined,
nodeKey: undefined,
configuration: undefined,
file: "request.json",
json: false,
});
});

it("should call getExamples with correct parameters", async () => {
const options: OptionValues = { assetType: "BOARD_V2", json: "" };
await (module as any).getExamples(testContext, mockCommand, options);
Expand Down
Loading
Loading