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
8 changes: 8 additions & 0 deletions command-snapshot.json
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,14 @@
"flags": ["api-version", "flags-dir", "json", "target-org", "template-id", "template-name"],
"plugin": "@salesforce/plugin-orchestrator"
},
{
"alias": [],
"command": "orchestrator:template:eval",
"flagAliases": [],
"flagChars": ["d", "o", "r", "v"],
"flags": ["api-version", "definition-file", "document-file", "flags-dir", "json", "target-org", "values-file"],
"plugin": "@salesforce/plugin-orchestrator"
},
{
"alias": [],
"command": "orchestrator:template:list",
Expand Down
58 changes: 58 additions & 0 deletions messages/orchestrator.template.eval.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# summary

Test JSON transformation rules using the jsonxform/transformation endpoint.

# description

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.

# flags.target-org.summary

Username or alias for the target org; overrides default target org.

# flags.target-org.description

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.

# flags.api-version.summary

Override the api version used for api requests made by this command.

# flags.api-version.description

API version to use for the transformation request. Defaults to the org's configured API version.

# flags.document-file.summary

Path to JSON document file to transform.

# flags.document-file.description

Path to the JSON document file that will be transformed by the rules.

# flags.values-file.summary

Path to JSON values file for variables.

# flags.values-file.description

Path to JSON file containing variables used in transformations.

# flags.definition-file.summary

Path to JSON rules definition file.

# flags.definition-file.description

Path to JSON file containing transformation rules and definitions.

# examples

- Test JSON transformation with document file only:
<%= config.bin %> <%= command.id %> --document-file ./document.json --target-org myorg

- Test with document, values, and rules files:
<%= config.bin %> <%= command.id %> --document-file ./document.json --values-file ./values.json --definition-file ./rules.json --target-org myorg

- Test with specific API version:
<%= config.bin %> <%= command.id %> --document-file ./document.json --target-org myorg --api-version 60.0
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"author": "Salesforce",
"bugs": "https://github.com/forcedotcom/cli/issues",
"dependencies": {
"@inquirer/select": "^5.0.1",
"@oclif/core": "^4",
"@salesforce/core": "^8.23.4",
"@salesforce/kit": "^3.2.1",
Expand Down Expand Up @@ -69,6 +70,9 @@
},
"orchestrator:template": {
"description": "Work with templates"
},
"template": {
"description": "description for template"
}
},
"flexibleTaxonomy": true
Expand Down
216 changes: 216 additions & 0 deletions src/commands/orchestrator/template/eval.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
/*
* Copyright 2025, Salesforce, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import * as fs from 'node:fs/promises';
import { SfCommand, Flags } from '@salesforce/sf-plugins-core';
import { Messages, Connection } from '@salesforce/core';

Messages.importMessagesDirectoryFromMetaUrl(import.meta.url);
const messages = Messages.loadMessages('@salesforce/plugin-orchestrator', 'orchestrator.template.eval');

type TransformationPayload = {
document: {
user: {
firstName: string;
lastName: string;
userName: string;
id: string;
hello: string;
};
company: {
id: string;
name: string;
namespace: string;
};
};
values: {
Variables: {
hello: string;
};
};
definition: {
rules: Array<{
name: string;
actions: Array<{
action: string;
description: string;
key: string;
path: string;
value: string;
}>;
}>;
};
};

type TemplateInfo = {
name: string;
path: string;
source: 'static' | 'local';
};

export type TemplatePreviewResult = {
status: 'success' | 'error';
template?: TemplateInfo;
input?: TransformationPayload;
output?: unknown;
error?: string;
executionTime?: string;
};

export default class TemplateEval extends SfCommand<TemplatePreviewResult> {
public static readonly summary = messages.getMessage('summary');
public static readonly description = messages.getMessage('description');
public static readonly examples = messages.getMessages('examples');

public static readonly flags = {
'target-org': Flags.requiredOrg({
summary: messages.getMessage('flags.target-org.summary'),
description: messages.getMessage('flags.target-org.description'),
required: true,
}),
'api-version': Flags.orgApiVersion({
summary: messages.getMessage('flags.api-version.summary'),
description: messages.getMessage('flags.api-version.description'),
}),
'document-file': Flags.file({
char: 'd',
summary: messages.getMessage('flags.document-file.summary'),
description: messages.getMessage('flags.document-file.description'),
required: true,
}),
'values-file': Flags.file({
char: 'v',
summary: messages.getMessage('flags.values-file.summary'),
description: messages.getMessage('flags.values-file.description'),
dependsOn: ['document-file'],
}),
'definition-file': Flags.file({
char: 'r',
summary: messages.getMessage('flags.definition-file.summary'),
description: messages.getMessage('flags.definition-file.description'),
dependsOn: ['document-file'],
}),
};

public async run(): Promise<TemplatePreviewResult> {
const { flags } = await this.parse(TemplateEval);

try {
type OrgType = { getConnection(apiVersion?: string): Connection };
const connection = (flags['target-org'] as OrgType).getConnection(flags['api-version']);

// Determine template source and payload
const templateResult = await this.getTemplatePayload(flags);

this.log(`Testing transformation: ${templateResult.template.name}`);
this.log(`Source: ${templateResult.template.source}`);

if (templateResult.template.source === 'local') {
this.log(`Path: ${templateResult.template.path}`);
}

const startTime = Date.now();

// Make request to jsonxform/transformation endpoint
const apiPath = `/services/data/v${connection.getApiVersion()}/jsonxform/transformation`;

const result = await connection.request({
method: 'POST',
url: apiPath,
body: JSON.stringify(templateResult.payload),
headers: {
'Content-Type': 'application/json',
},
});

const executionTime = `${Date.now() - startTime}ms`;

this.log('Transformation completed successfully!');
this.log('Results:');
this.log(JSON.stringify(result, null, 2));

return {
status: 'success',
template: templateResult.template,
input: templateResult.payload,
output: result,
executionTime,
};
} catch (error) {
this.log(`Transformation failed: ${(error as Error).message}`);

return {
status: 'error',
error: (error as Error).message,
};
}
}

private async getTemplatePayload(flags: {
'document-file': string;
'values-file'?: string;
'definition-file'?: string;
}): Promise<{
template: TemplateInfo;
payload: TransformationPayload;
}> {
return this.getDirectFilePayload(flags['document-file'], flags['values-file'], flags['definition-file']);
}

private async getDirectFilePayload(
documentFile: string,
valuesFile?: string,
definitionFile?: string
): Promise<{
template: TemplateInfo;
payload: TransformationPayload;
}> {
this.log(`Loading document: ${documentFile}`);

// Read and parse the document file
const documentContent = await fs.readFile(documentFile, 'utf8');
const document = JSON.parse(documentContent) as unknown;

// Read values file if provided, otherwise use empty object
let values = { Variables: { hello: 'world' } };
if (valuesFile) {
this.log(`Loading values: ${valuesFile}`);
const valuesContent = await fs.readFile(valuesFile, 'utf8');
values = JSON.parse(valuesContent) as typeof values;
}

// Read definition file if provided, otherwise use empty rules
let definition = { rules: [] };
if (definitionFile) {
this.log(`Loading definition: ${definitionFile}`);
const definitionContent = await fs.readFile(definitionFile, 'utf8');
definition = JSON.parse(definitionContent) as typeof definition;
}

return {
template: {
name: 'Direct Files',
path: documentFile,
source: 'local' as const,
},
payload: {
document: document as TransformationPayload['document'],
values: values as TransformationPayload['values'],
definition: definition as TransformationPayload['definition'],
},
};
}
}
36 changes: 36 additions & 0 deletions test/commands/template/preview.nut.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
* Copyright 2025, Salesforce, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { execCmd, TestSession } from '@salesforce/cli-plugins-testkit';
import { expect } from 'chai';

describe('template preview NUTs', () => {
let session: TestSession;

before(async () => {
session = await TestSession.create({ devhubAuthStrategy: 'NONE' });
});

after(async () => {
await session?.clean();
});

it('should display provided name', () => {
const name = 'World';
const command = `template preview --name ${name}`;
const output = execCmd(command, { ensureExitCode: 0 }).shellOutput.stdout;
expect(output).to.contain(name);
});
});
30 changes: 30 additions & 0 deletions test/commands/template/preview.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
* Copyright 2025, Salesforce, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { TestContext } from '@salesforce/core/testSetup';
import { expect } from 'chai';
import TemplateEval from '../../../src/commands/orchestrator/template/eval.js';

describe('template eval', () => {
const $$ = new TestContext();

afterEach(() => {
$$.restore();
});

it('should exist and be importable', async () => {
expect(TemplateEval).to.exist;
});
});
Loading
Loading