Skip to content

Commit 1fd287a

Browse files
committed
Add asset-registry validate command
Add `asset-registry validate --assetType X` that proxies to the new Pacman validate endpoint (POST /core/asset-registry/validate/{assetType}). Accepts --body for inline JSON or -f for a JSON file. Update SKILL.md, agentic guide, and command docs. Includes-AI-Code: true Made-with: Cursor
1 parent 2c23fc9 commit 1fd287a

8 files changed

Lines changed: 234 additions & 12 deletions

File tree

.cursor/skills/asset-registry-endpoints/SKILL.md

Lines changed: 33 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -223,20 +223,44 @@ methodology — a 404 means the endpoint is not available.
223223
$CLI asset-registry methodology --assetType <ASSET_TYPE> -p <profile>
224224
```
225225

226-
### Validate (POST — via config import)
226+
### Validate (POST)
227227

228-
Use `config import --validate` to validate assets against their schema before
229-
importing:
228+
Validate asset configurations against the asset service's validation endpoint.
229+
The request body follows the `ValidateRequest` schema:
230+
231+
```json
232+
{
233+
"assetType": "BOARD_V2",
234+
"packageKey": "my-pkg",
235+
"nodes": [
236+
{ "key": "my-view", "configuration": { ... } }
237+
]
238+
}
239+
```
240+
241+
Inline:
230242

231243
```bash
232-
$CLI config import -d <export_dir> --validate --overwrite -p <profile>
244+
$CLI asset-registry validate --assetType <ASSET_TYPE> --body '<json>' -p <profile>
245+
```
246+
247+
From file:
248+
249+
```bash
250+
$CLI asset-registry validate --assetType <ASSET_TYPE> -f request.json -p <profile>
233251
```
234252

235253
**Important**: If validation returns errors, do **not** proceed with the import.
236-
Instead, fix the schema violations in the node JSON and re-run the command. If
237-
you cannot resolve the errors automatically, present the validation results to
238-
the user and ask whether they want to continue importing with invalid
239-
configuration or stop to fix it manually.
254+
Instead, fix the schema violations in the node JSON and re-validate. If you
255+
cannot resolve the errors automatically, present the validation results to the
256+
user and ask whether they want to continue importing with invalid configuration
257+
or stop to fix it manually.
258+
259+
You can also validate during import with `config import --validate`:
260+
261+
```bash
262+
$CLI config import -d <export_dir> --validate --overwrite -p <profile>
263+
```
240264

241265
## Troubleshooting
242266

@@ -284,6 +308,7 @@ $CLI config import -d <export_dir> --validate --overwrite -p <profile>
284308
| `asset-registry list` | List all registered asset types |
285309
| `asset-registry get --assetType X` | Get the full descriptor for an asset type |
286310
| `asset-registry schema --assetType X` | Get the JSON Schema for the asset's configuration |
311+
| `asset-registry validate --assetType X --body/​-f` | Validate asset configurations against the asset service |
287312
| `asset-registry examples --assetType X` | Get example configurations (if available) |
288313
| `asset-registry methodology --assetType X` | Get methodology / best-practices (if available) |
289314
| `config list` | List packages |

docs/user-guide/agentic-development-guide.md

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -73,15 +73,26 @@ Add a new JSON file in the `nodes/` directory:
7373

7474
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.
7575

76-
### 5. Validate and import
76+
### 5. Validate
77+
78+
Before importing, validate the asset configuration:
79+
80+
```bash
81+
content-cli asset-registry validate --assetType <ASSET_TYPE> \
82+
--body '{"assetType":"<ASSET_TYPE>","packageKey":"<pkg>","nodes":[{"key":"<key>","configuration":{...}}]}'
83+
```
84+
85+
Or validate during import with the `--validate` flag:
7786

7887
```bash
7988
content-cli config import -d <export_dir> --validate --overwrite
8089
```
8190

82-
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.
91+
If validation returns errors, fix the issues before importing.
92+
93+
### 6. Import
8394

84-
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`.
95+
This creates a new version in staging (not deployed). To create a brand-new package instead of updating, omit `--overwrite`.
8596

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

docs/user-guide/asset-registry-commands.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,29 @@ Options:
7171
- `--assetType <assetType>` (required) – The asset type identifier
7272
- `--json` – Write the schema to a JSON file in the working directory
7373

74+
## Validate
75+
76+
Validate asset configurations against the asset service's validation endpoint. The request body must follow the `ValidateRequest` format expected by the asset service.
77+
78+
Using inline JSON:
79+
80+
```
81+
content-cli asset-registry validate --assetType BOARD_V2 --body '{"assetType":"BOARD_V2","packageKey":"my-pkg","nodes":[{"key":"my-view","configuration":{}}]}'
82+
```
83+
84+
Using a JSON file:
85+
86+
```
87+
content-cli asset-registry validate --assetType BOARD_V2 -f request.json
88+
```
89+
90+
Options:
91+
92+
- `--assetType <assetType>` (required) – The asset type identifier
93+
- `--body <body>` – Validation request body as inline JSON
94+
- `-f, --file <file>` – Path to a JSON file containing the validation request body
95+
- `--json` – Write the validation response to a JSON file in the working directory
96+
7497
## Get Examples
7598

7699
Fetch example configurations for an asset type. Not all asset types provide examples.

src/commands/asset-registry/asset-registry-api.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,14 @@ export class AssetRegistryApi {
4242
});
4343
}
4444

45+
public async validate(assetType: string, body: any): Promise<any> {
46+
return this.httpClient()
47+
.post(`/pacman/api/core/asset-registry/validate/${encodeURIComponent(assetType)}`, body)
48+
.catch((e) => {
49+
throw new FatalError(`Problem validating asset type '${assetType}': ${e}`);
50+
});
51+
}
52+
4553
public async getMethodology(assetType: string): Promise<any> {
4654
return this.httpClient()
4755
.get(`/pacman/api/core/asset-registry/methodologies/${encodeURIComponent(assetType)}`)

src/commands/asset-registry/asset-registry.service.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@ import { AssetRegistryApi } from "./asset-registry-api";
22
import { AssetRegistryDescriptor } from "./asset-registry.interfaces";
33
import { Context } from "../../core/command/cli-context";
44
import { fileService, FileService } from "../../core/utils/file-service";
5-
import { logger } from "../../core/utils/logger";
5+
import { FatalError, logger } from "../../core/utils/logger";
66
import { v4 as uuidv4 } from "uuid";
7+
import * as fs from "fs";
78

89
export class AssetRegistryService {
910
private api: AssetRegistryApi;
@@ -53,6 +54,12 @@ export class AssetRegistryService {
5354
this.outputResponse(data, jsonResponse);
5455
}
5556

57+
public async validate(assetType: string, body?: string, file?: string, jsonResponse?: boolean): Promise<void> {
58+
const payload = this.resolveBody(body, file);
59+
const data = await this.api.validate(assetType, payload);
60+
this.outputResponse(data, !!jsonResponse);
61+
}
62+
5663
public async getMethodology(assetType: string, jsonResponse: boolean): Promise<void> {
5764
const data = await this.api.getMethodology(assetType);
5865
this.outputResponse(data, jsonResponse);
@@ -93,4 +100,20 @@ export class AssetRegistryService {
93100
logger.info(` examples: ${descriptor.endpoints.examples}`);
94101
}
95102
}
103+
104+
private resolveBody(body?: string, file?: string): any {
105+
if (!body && !file) {
106+
throw new FatalError("Provide either --body (inline JSON) or -f (path to JSON file).");
107+
}
108+
if (body && file) {
109+
throw new FatalError("Provide either --body or -f, not both.");
110+
}
111+
112+
const raw = file ? fs.readFileSync(file, "utf-8") : body!;
113+
try {
114+
return JSON.parse(raw);
115+
} catch {
116+
throw new FatalError("Request body must be valid JSON.");
117+
}
118+
}
96119
}

src/commands/asset-registry/module.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,14 @@ class Module extends IModule {
3232
.option("--json", "Return the response as a JSON file")
3333
.action(this.getExamples);
3434

35+
assetRegistryCommand.command("validate")
36+
.description("Validate asset configuration against the asset service")
37+
.requiredOption("--assetType <assetType>", "The asset type identifier (e.g., BOARD_V2)")
38+
.option("--body <body>", "Validation request body as inline JSON")
39+
.option("-f, --file <file>", "Path to a JSON file containing the validation request body")
40+
.option("--json", "Return the response as a JSON file")
41+
.action(this.validate);
42+
3543
assetRegistryCommand.command("methodology")
3644
.description("Get the methodology / best-practices guide for an asset type")
3745
.requiredOption("--assetType <assetType>", "The asset type identifier (e.g., BOARD_V2)")
@@ -51,6 +59,10 @@ class Module extends IModule {
5159
await new AssetRegistryService(context).getSchema(options.assetType, !!options.json);
5260
}
5361

62+
private async validate(context: Context, command: Command, options: OptionValues): Promise<void> {
63+
await new AssetRegistryService(context).validate(options.assetType, options.body, options.file, !!options.json);
64+
}
65+
5466
private async getExamples(context: Context, command: Command, options: OptionValues): Promise<void> {
5567
await new AssetRegistryService(context).getExamples(options.assetType, !!options.json);
5668
}

tests/commands/asset-registry/asset-registry-module.spec.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ describe("Asset Registry Module", () => {
1919
listTypes: jest.fn().mockResolvedValue(undefined),
2020
getType: jest.fn().mockResolvedValue(undefined),
2121
getSchema: jest.fn().mockResolvedValue(undefined),
22+
validate: jest.fn().mockResolvedValue(undefined),
2223
getExamples: jest.fn().mockResolvedValue(undefined),
2324
getMethodology: jest.fn().mockResolvedValue(undefined),
2425
} as any;
@@ -33,6 +34,18 @@ describe("Asset Registry Module", () => {
3334
expect(mockService.getSchema).toHaveBeenCalledWith("BOARD_V2", true);
3435
});
3536

37+
it("should call validate with correct parameters", async () => {
38+
const options: OptionValues = { assetType: "BOARD_V2", body: '{"nodes":[]}', json: "" };
39+
await (module as any).validate(testContext, mockCommand, options);
40+
expect(mockService.validate).toHaveBeenCalledWith("BOARD_V2", '{"nodes":[]}', undefined, false);
41+
});
42+
43+
it("should call validate with file option", async () => {
44+
const options: OptionValues = { assetType: "BOARD_V2", file: "request.json", json: true };
45+
await (module as any).validate(testContext, mockCommand, options);
46+
expect(mockService.validate).toHaveBeenCalledWith("BOARD_V2", undefined, "request.json", true);
47+
});
48+
3649
it("should call getExamples with correct parameters", async () => {
3750
const options: OptionValues = { assetType: "BOARD_V2", json: "" };
3851
await (module as any).getExamples(testContext, mockCommand, options);
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import { mockAxiosPost } from "../../utls/http-requests-mock";
2+
import { AssetRegistryService } from "../../../src/commands/asset-registry/asset-registry.service";
3+
import { testContext } from "../../utls/test-context";
4+
import { loggingTestTransport, mockWriteFileSync } from "../../jest.setup";
5+
import { FileService } from "../../../src/core/utils/file-service";
6+
import * as path from "path";
7+
import * as fs from "fs";
8+
9+
jest.mock("fs", () => ({
10+
...jest.requireActual("fs"),
11+
readFileSync: jest.fn(),
12+
}));
13+
14+
describe("Asset registry validate", () => {
15+
const validateResponse = {
16+
valid: false,
17+
diagnostics: [
18+
{
19+
severity: "ERROR",
20+
nodeKey: "my-view",
21+
assetType: "BOARD_V2",
22+
path: "$.components[0].type",
23+
code: "INVALID_ENUM_VALUE",
24+
message: "Invalid component type",
25+
},
26+
],
27+
};
28+
29+
const requestBody = {
30+
assetType: "BOARD_V2",
31+
packageKey: "my-pkg",
32+
nodes: [{ key: "my-view", configuration: { components: [{ type: "bad" }] } }],
33+
};
34+
35+
it("Should validate with inline body and print result", async () => {
36+
mockAxiosPost(
37+
"https://myTeam.celonis.cloud/pacman/api/core/asset-registry/validate/BOARD_V2",
38+
validateResponse
39+
);
40+
41+
await new AssetRegistryService(testContext).validate(
42+
"BOARD_V2", JSON.stringify(requestBody), undefined, false
43+
);
44+
45+
expect(loggingTestTransport.logMessages.length).toBe(1);
46+
const output = loggingTestTransport.logMessages[0].message;
47+
expect(output).toContain("INVALID_ENUM_VALUE");
48+
expect(output).toContain("my-view");
49+
});
50+
51+
it("Should validate with inline body and save as JSON file", async () => {
52+
mockAxiosPost(
53+
"https://myTeam.celonis.cloud/pacman/api/core/asset-registry/validate/BOARD_V2",
54+
validateResponse
55+
);
56+
57+
await new AssetRegistryService(testContext).validate(
58+
"BOARD_V2", JSON.stringify(requestBody), undefined, true
59+
);
60+
61+
const expectedFileName = loggingTestTransport.logMessages[0].message.split(FileService.fileDownloadedMessage)[1];
62+
expect(mockWriteFileSync).toHaveBeenCalledWith(
63+
path.resolve(process.cwd(), expectedFileName),
64+
expect.any(String),
65+
{ encoding: "utf-8" }
66+
);
67+
68+
const written = JSON.parse(mockWriteFileSync.mock.calls[0][1]);
69+
expect(written.valid).toBe(false);
70+
expect(written.diagnostics[0].code).toBe("INVALID_ENUM_VALUE");
71+
});
72+
73+
it("Should validate with body from file", async () => {
74+
(fs.readFileSync as jest.Mock).mockReturnValue(JSON.stringify(requestBody));
75+
76+
mockAxiosPost(
77+
"https://myTeam.celonis.cloud/pacman/api/core/asset-registry/validate/BOARD_V2",
78+
validateResponse
79+
);
80+
81+
await new AssetRegistryService(testContext).validate(
82+
"BOARD_V2", undefined, "request.json", false
83+
);
84+
85+
expect(fs.readFileSync).toHaveBeenCalledWith("request.json", "utf-8");
86+
expect(loggingTestTransport.logMessages.length).toBe(1);
87+
expect(loggingTestTransport.logMessages[0].message).toContain("INVALID_ENUM_VALUE");
88+
});
89+
90+
it("Should throw when neither body nor file is provided", async () => {
91+
await expect(
92+
new AssetRegistryService(testContext).validate("BOARD_V2", undefined, undefined, false)
93+
).rejects.toThrow("Provide either --body (inline JSON) or -f (path to JSON file).");
94+
});
95+
96+
it("Should throw when both body and file are provided", async () => {
97+
await expect(
98+
new AssetRegistryService(testContext).validate("BOARD_V2", "{}", "file.json", false)
99+
).rejects.toThrow("Provide either --body or -f, not both.");
100+
});
101+
102+
it("Should throw when body is not valid JSON", async () => {
103+
await expect(
104+
new AssetRegistryService(testContext).validate("BOARD_V2", "not-json{", undefined, false)
105+
).rejects.toThrow("Request body must be valid JSON.");
106+
});
107+
});

0 commit comments

Comments
 (0)