Skip to content

Commit 96f957f

Browse files
authored
Viera/add template eval command (#115)
- adds eval command to cli
1 parent 54ecac9 commit 96f957f

File tree

7 files changed

+3143
-2330
lines changed

7 files changed

+3143
-2330
lines changed

command-snapshot.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,14 @@
9595
"flags": ["api-version", "flags-dir", "json", "target-org", "template-id", "template-name"],
9696
"plugin": "@salesforce/plugin-orchestrator"
9797
},
98+
{
99+
"alias": [],
100+
"command": "orchestrator:template:eval",
101+
"flagAliases": [],
102+
"flagChars": ["d", "o", "r", "v"],
103+
"flags": ["api-version", "definition-file", "document-file", "flags-dir", "json", "target-org", "values-file"],
104+
"plugin": "@salesforce/plugin-orchestrator"
105+
},
98106
{
99107
"alias": [],
100108
"command": "orchestrator:template:list",
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# summary
2+
3+
Test JSON transformation rules using the jsonxform/transformation endpoint.
4+
5+
# description
6+
7+
Preview how transformation rules will modify JSON documents before deploying templates. This command uses a sample transformation to test the jsonxform/transformation endpoint with built-in User and Org context variables.
8+
9+
# flags.target-org.summary
10+
11+
Username or alias for the target org; overrides default target org.
12+
13+
# flags.target-org.description
14+
15+
The username or alias of the target org where the jsonxform/transformation endpoint will be called. This org provides the User and Org context variables used in the transformation.
16+
17+
# flags.api-version.summary
18+
19+
Override the api version used for api requests made by this command.
20+
21+
# flags.api-version.description
22+
23+
API version to use for the transformation request. Defaults to the org's configured API version.
24+
25+
# flags.document-file.summary
26+
27+
Path to JSON document file to transform.
28+
29+
# flags.document-file.description
30+
31+
Path to the JSON document file that will be transformed by the rules.
32+
33+
# flags.values-file.summary
34+
35+
Path to JSON values file for variables.
36+
37+
# flags.values-file.description
38+
39+
Path to JSON file containing variables used in transformations.
40+
41+
# flags.definition-file.summary
42+
43+
Path to JSON rules definition file.
44+
45+
# flags.definition-file.description
46+
47+
Path to JSON file containing transformation rules and definitions.
48+
49+
# examples
50+
51+
- Test JSON transformation with document file only:
52+
<%= config.bin %> <%= command.id %> --document-file ./document.json --target-org myorg
53+
54+
- Test with document, values, and rules files:
55+
<%= config.bin %> <%= command.id %> --document-file ./document.json --values-file ./values.json --definition-file ./rules.json --target-org myorg
56+
57+
- Test with specific API version:
58+
<%= config.bin %> <%= command.id %> --document-file ./document.json --target-org myorg --api-version 60.0

package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
"author": "Salesforce",
66
"bugs": "https://github.com/forcedotcom/cli/issues",
77
"dependencies": {
8+
"@inquirer/select": "^5.0.1",
89
"@oclif/core": "^4",
910
"@salesforce/core": "^8.23.4",
1011
"@salesforce/kit": "^3.2.1",
@@ -69,6 +70,9 @@
6970
},
7071
"orchestrator:template": {
7172
"description": "Work with templates"
73+
},
74+
"template": {
75+
"description": "description for template"
7276
}
7377
},
7478
"flexibleTaxonomy": true
Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
/*
2+
* Copyright 2025, Salesforce, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import * as fs from 'node:fs/promises';
18+
import { SfCommand, Flags } from '@salesforce/sf-plugins-core';
19+
import { Messages, Connection } from '@salesforce/core';
20+
21+
Messages.importMessagesDirectoryFromMetaUrl(import.meta.url);
22+
const messages = Messages.loadMessages('@salesforce/plugin-orchestrator', 'orchestrator.template.eval');
23+
24+
type TransformationPayload = {
25+
document: {
26+
user: {
27+
firstName: string;
28+
lastName: string;
29+
userName: string;
30+
id: string;
31+
hello: string;
32+
};
33+
company: {
34+
id: string;
35+
name: string;
36+
namespace: string;
37+
};
38+
};
39+
values: {
40+
Variables: {
41+
hello: string;
42+
};
43+
};
44+
definition: {
45+
rules: Array<{
46+
name: string;
47+
actions: Array<{
48+
action: string;
49+
description: string;
50+
key: string;
51+
path: string;
52+
value: string;
53+
}>;
54+
}>;
55+
};
56+
};
57+
58+
type TemplateInfo = {
59+
name: string;
60+
path: string;
61+
source: 'static' | 'local';
62+
};
63+
64+
export type TemplatePreviewResult = {
65+
status: 'success' | 'error';
66+
template?: TemplateInfo;
67+
input?: TransformationPayload;
68+
output?: unknown;
69+
error?: string;
70+
executionTime?: string;
71+
};
72+
73+
export default class TemplateEval extends SfCommand<TemplatePreviewResult> {
74+
public static readonly summary = messages.getMessage('summary');
75+
public static readonly description = messages.getMessage('description');
76+
public static readonly examples = messages.getMessages('examples');
77+
78+
public static readonly flags = {
79+
'target-org': Flags.requiredOrg({
80+
summary: messages.getMessage('flags.target-org.summary'),
81+
description: messages.getMessage('flags.target-org.description'),
82+
required: true,
83+
}),
84+
'api-version': Flags.orgApiVersion({
85+
summary: messages.getMessage('flags.api-version.summary'),
86+
description: messages.getMessage('flags.api-version.description'),
87+
}),
88+
'document-file': Flags.file({
89+
char: 'd',
90+
summary: messages.getMessage('flags.document-file.summary'),
91+
description: messages.getMessage('flags.document-file.description'),
92+
required: true,
93+
}),
94+
'values-file': Flags.file({
95+
char: 'v',
96+
summary: messages.getMessage('flags.values-file.summary'),
97+
description: messages.getMessage('flags.values-file.description'),
98+
dependsOn: ['document-file'],
99+
}),
100+
'definition-file': Flags.file({
101+
char: 'r',
102+
summary: messages.getMessage('flags.definition-file.summary'),
103+
description: messages.getMessage('flags.definition-file.description'),
104+
dependsOn: ['document-file'],
105+
}),
106+
};
107+
108+
public async run(): Promise<TemplatePreviewResult> {
109+
const { flags } = await this.parse(TemplateEval);
110+
111+
try {
112+
type OrgType = { getConnection(apiVersion?: string): Connection };
113+
const connection = (flags['target-org'] as OrgType).getConnection(flags['api-version']);
114+
115+
// Determine template source and payload
116+
const templateResult = await this.getTemplatePayload(flags);
117+
118+
this.log(`Testing transformation: ${templateResult.template.name}`);
119+
this.log(`Source: ${templateResult.template.source}`);
120+
121+
if (templateResult.template.source === 'local') {
122+
this.log(`Path: ${templateResult.template.path}`);
123+
}
124+
125+
const startTime = Date.now();
126+
127+
// Make request to jsonxform/transformation endpoint
128+
const apiPath = `/services/data/v${connection.getApiVersion()}/jsonxform/transformation`;
129+
130+
const result = await connection.request({
131+
method: 'POST',
132+
url: apiPath,
133+
body: JSON.stringify(templateResult.payload),
134+
headers: {
135+
'Content-Type': 'application/json',
136+
},
137+
});
138+
139+
const executionTime = `${Date.now() - startTime}ms`;
140+
141+
this.log('Transformation completed successfully!');
142+
this.log('Results:');
143+
this.log(JSON.stringify(result, null, 2));
144+
145+
return {
146+
status: 'success',
147+
template: templateResult.template,
148+
input: templateResult.payload,
149+
output: result,
150+
executionTime,
151+
};
152+
} catch (error) {
153+
this.log(`Transformation failed: ${(error as Error).message}`);
154+
155+
return {
156+
status: 'error',
157+
error: (error as Error).message,
158+
};
159+
}
160+
}
161+
162+
private async getTemplatePayload(flags: {
163+
'document-file': string;
164+
'values-file'?: string;
165+
'definition-file'?: string;
166+
}): Promise<{
167+
template: TemplateInfo;
168+
payload: TransformationPayload;
169+
}> {
170+
return this.getDirectFilePayload(flags['document-file'], flags['values-file'], flags['definition-file']);
171+
}
172+
173+
private async getDirectFilePayload(
174+
documentFile: string,
175+
valuesFile?: string,
176+
definitionFile?: string
177+
): Promise<{
178+
template: TemplateInfo;
179+
payload: TransformationPayload;
180+
}> {
181+
this.log(`Loading document: ${documentFile}`);
182+
183+
// Read and parse the document file
184+
const documentContent = await fs.readFile(documentFile, 'utf8');
185+
const document = JSON.parse(documentContent) as unknown;
186+
187+
// Read values file if provided, otherwise use empty object
188+
let values = { Variables: { hello: 'world' } };
189+
if (valuesFile) {
190+
this.log(`Loading values: ${valuesFile}`);
191+
const valuesContent = await fs.readFile(valuesFile, 'utf8');
192+
values = JSON.parse(valuesContent) as typeof values;
193+
}
194+
195+
// Read definition file if provided, otherwise use empty rules
196+
let definition = { rules: [] };
197+
if (definitionFile) {
198+
this.log(`Loading definition: ${definitionFile}`);
199+
const definitionContent = await fs.readFile(definitionFile, 'utf8');
200+
definition = JSON.parse(definitionContent) as typeof definition;
201+
}
202+
203+
return {
204+
template: {
205+
name: 'Direct Files',
206+
path: documentFile,
207+
source: 'local' as const,
208+
},
209+
payload: {
210+
document: document as TransformationPayload['document'],
211+
values: values as TransformationPayload['values'],
212+
definition: definition as TransformationPayload['definition'],
213+
},
214+
};
215+
}
216+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/*
2+
* Copyright 2025, Salesforce, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
import { execCmd, TestSession } from '@salesforce/cli-plugins-testkit';
17+
import { expect } from 'chai';
18+
19+
describe('template preview NUTs', () => {
20+
let session: TestSession;
21+
22+
before(async () => {
23+
session = await TestSession.create({ devhubAuthStrategy: 'NONE' });
24+
});
25+
26+
after(async () => {
27+
await session?.clean();
28+
});
29+
30+
it('should display provided name', () => {
31+
const name = 'World';
32+
const command = `template preview --name ${name}`;
33+
const output = execCmd(command, { ensureExitCode: 0 }).shellOutput.stdout;
34+
expect(output).to.contain(name);
35+
});
36+
});
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/*
2+
* Copyright 2025, Salesforce, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
import { TestContext } from '@salesforce/core/testSetup';
17+
import { expect } from 'chai';
18+
import TemplateEval from '../../../src/commands/orchestrator/template/eval.js';
19+
20+
describe('template eval', () => {
21+
const $$ = new TestContext();
22+
23+
afterEach(() => {
24+
$$.restore();
25+
});
26+
27+
it('should exist and be importable', async () => {
28+
expect(TemplateEval).to.exist;
29+
});
30+
});

0 commit comments

Comments
 (0)