-
Notifications
You must be signed in to change notification settings - Fork 13
feat: add --context-variables-json for typed preview context variables #483
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
418a13c
22257ca
be52c7b
19393f3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -19,7 +19,13 @@ import { EnvironmentVariable, Lifecycle, Messages, SfError } from '@salesforce/c | |
| import { Agent, ProductionAgent, ScriptAgent } from '@salesforce/agents'; | ||
| import { createCache, SessionType } from '../../../previewSessionStore.js'; | ||
| import { COMPILATION_API_EXIT_CODES, loadAgentJson } from '../../../common.js'; | ||
| import { contextVariablesFlag, parseContextVariables } from '../../../flags.js'; | ||
| import { | ||
| contextVariablesFlag, | ||
| contextVariablesJsonFlag, | ||
| mergeContextVariables, | ||
| parseContextVariables, | ||
| parseContextVariablesJson, | ||
| } from '../../../flags.js'; | ||
|
|
||
| Messages.importMessagesDirectoryFromMetaUrl(import.meta.url); | ||
| const messages = Messages.loadMessages('@salesforce/plugin-agent', 'agent.preview.start'); | ||
|
|
@@ -70,6 +76,7 @@ export default class AgentPreviewStart extends SfCommand<AgentPreviewStartResult | |
| exclusive: ['use-live-actions'], | ||
| }), | ||
| 'context-variables': contextVariablesFlag, | ||
| 'context-variables-json': contextVariablesJsonFlag, | ||
| 'agent-json': Flags.file({ | ||
| summary: messages.getMessage('flags.agent-json.summary'), | ||
| hidden: true, | ||
|
|
@@ -159,7 +166,10 @@ export default class AgentPreviewStart extends SfCommand<AgentPreviewStartResult | |
| } | ||
|
|
||
| // Track telemetry for preview start | ||
| const contextVariables = parseContextVariables(flags['context-variables']); | ||
| const contextVariables = mergeContextVariables( | ||
| parseContextVariables(flags['context-variables']), | ||
| parseContextVariablesJson(flags['context-variables-json']) | ||
| ); | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When merging |
||
| let session; | ||
| try { | ||
| session = await agent.preview.start({ contextVariables }); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -23,7 +23,12 @@ import { Connection, Messages, SfError, SfProject } from '@salesforce/core'; | |
| import { camelCaseToTitleCase } from '@salesforce/kit'; | ||
| import { select, input as inquirerInput } from '@inquirer/prompts'; | ||
| import autocomplete from 'inquirer-autocomplete-standalone'; | ||
| import { AgentTest, AgentTestResultsResponse, type ContextVariable } from '@salesforce/agents'; | ||
| import { | ||
| AgentTest, | ||
| AgentTestResultsResponse, | ||
| type ContextVariable, | ||
| type ContextVariableType, | ||
| } from '@salesforce/agents'; | ||
| import { theme } from './inquirer-theme.js'; | ||
| import { AgentTestResultsResult } from './commands/agent/test/results.js'; | ||
|
|
||
|
|
@@ -81,10 +86,126 @@ export const contextVariablesFlag = Flags.string({ | |
| description: messages.getMessage('flags.context-variables.description'), | ||
| }); | ||
|
|
||
| /** | ||
| * JSON form of --context-variables that carries the variable's type, so callers can | ||
| * send Boolean/Number/Object/List/Json values (not just Text). Deliberately has no | ||
| * `delimiter`, so a comma inside the JSON (or inside a List/Object value) is safe. | ||
| */ | ||
| export const contextVariablesJsonFlag = Flags.string({ | ||
| summary: messages.getMessage('flags.context-variables-json.summary'), | ||
| description: messages.getMessage('flags.context-variables-json.description'), | ||
| }); | ||
|
|
||
| // The valid ContextVariable.type values, mirroring the preview API's Variable schema. | ||
| const CONTEXT_VARIABLE_TYPES: readonly ContextVariableType[] = [ | ||
| 'Text', | ||
| 'Date', | ||
| 'DateTime', | ||
| 'Money', | ||
| 'Ref', | ||
| 'Boolean', | ||
| 'Number', | ||
| 'Object', | ||
| 'List', | ||
| 'Json', | ||
| ]; | ||
|
|
||
| // Types whose `value` is a plain string on the wire. | ||
| const STRING_CONTEXT_VARIABLE_TYPES: readonly ContextVariableType[] = ['Text', 'Date', 'DateTime', 'Money', 'Ref']; | ||
|
|
||
| function describeJsonValue(value: unknown): string { | ||
| if (value === null) return 'null'; | ||
| if (Array.isArray(value)) return 'an array'; | ||
| return `a ${typeof value}`; | ||
| } | ||
|
|
||
| /** | ||
| * Validates that a decoded JSON `value` matches its declared `type`, matching the | ||
| * preview API's per-type Variable schema (Boolean->boolean, Number->number, | ||
| * string types->string, Object/List->array, Json->object). `value` is optional and | ||
| * nullable, so undefined/null pass. | ||
| */ | ||
| function validateContextVariableValue(name: string, type: ContextVariableType, value: unknown): void { | ||
| if (value === undefined || value === null) return; | ||
| const reject = (expected: string): never => { | ||
| throw new SfError( | ||
| `Invalid --context-variables-json: variable "${name}" of type "${type}" expects ${expected}, but got ${describeJsonValue( | ||
| value | ||
| )}.` | ||
| ); | ||
| }; | ||
| if (type === 'Boolean' && typeof value !== 'boolean') reject('a boolean value'); | ||
| else if (type === 'Number' && typeof value !== 'number') reject('a number value'); | ||
| else if (STRING_CONTEXT_VARIABLE_TYPES.includes(type) && typeof value !== 'string') reject('a string value'); | ||
| else if ((type === 'Object' || type === 'List') && !Array.isArray(value)) reject('an array value'); | ||
| else if (type === 'Json' && (typeof value !== 'object' || Array.isArray(value))) reject('a JSON object value'); | ||
| } | ||
|
|
||
| function toContextVariable(entry: unknown, index: number): ContextVariable { | ||
| if (typeof entry !== 'object' || entry === null || Array.isArray(entry)) { | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Array.isArray is just checking the format is not an array since it must be a json object. |
||
| throw new SfError( | ||
| `Invalid --context-variables-json: entry at index ${index} must be an object with "name" and "type" (and optionally "value").` | ||
| ); | ||
| } | ||
| const { name, type, value } = entry as Record<string, unknown>; | ||
| if (typeof name !== 'string' || name.trim() === '') { | ||
| throw new SfError(`Invalid --context-variables-json: entry at index ${index} is missing a non-empty "name".`); | ||
| } | ||
| if (typeof type !== 'string' || !CONTEXT_VARIABLE_TYPES.includes(type as ContextVariableType)) { | ||
| throw new SfError( | ||
| `Invalid --context-variables-json: variable "${name}" has invalid type "${String( | ||
| type | ||
| )}". Expected one of: ${CONTEXT_VARIABLE_TYPES.join(', ')}.` | ||
| ); | ||
| } | ||
| validateContextVariableValue(name, type as ContextVariableType, value); | ||
| return { name, type, value } as ContextVariable; | ||
| } | ||
|
|
||
| const CONTEXT_VARIABLES_JSON_EXAMPLE = '[{"name":"probeGate","type":"Boolean","value":true}]'; | ||
|
|
||
| /** | ||
| * Parses the --context-variables-json flag: a JSON array of typed context variables | ||
| * ({ name, type, value }) matching the preview API's Variable schema. Throws an | ||
| * SfError with a specific reason on malformed JSON, a non-array, or a bad entry. | ||
| */ | ||
| export function parseContextVariablesJson(raw: string | undefined): ContextVariable[] { | ||
| if (raw === undefined || raw.trim() === '') return []; | ||
| let parsed: unknown; | ||
| try { | ||
| parsed = JSON.parse(raw); | ||
| } catch { | ||
| throw new SfError( | ||
| `Invalid --context-variables-json: value is not valid JSON. Expected a JSON array, e.g. ${CONTEXT_VARIABLES_JSON_EXAMPLE}.` | ||
| ); | ||
| } | ||
| if (!Array.isArray(parsed)) { | ||
| throw new SfError( | ||
| `Invalid --context-variables-json: expected a JSON array, e.g. ${CONTEXT_VARIABLES_JSON_EXAMPLE}.` | ||
| ); | ||
| } | ||
| return parsed.map(toContextVariable); | ||
| } | ||
|
|
||
| /** | ||
| * Merges the text-form (--context-variables) and JSON-form (--context-variables-json) | ||
| * context variables into one array. When the same name appears in both, the JSON entry | ||
| * wins, keeping the text entry's original position. | ||
| */ | ||
| export function mergeContextVariables( | ||
| textVariables: ContextVariable[], | ||
| jsonVariables: ContextVariable[] | ||
| ): ContextVariable[] { | ||
| const byName = new Map<string, ContextVariable>(); | ||
| for (const variable of textVariables) byName.set(variable.name, variable); | ||
| for (const variable of jsonVariables) byName.set(variable.name, variable); | ||
| return [...byName.values()]; | ||
| } | ||
|
|
||
| /** | ||
| * Parses raw "Name=Value" entries from --context-variables into ContextVariable | ||
| * objects for the SDK. Type defaults to "Text" — the only empirically-observed | ||
| * variant on the wire today. | ||
| * objects for the SDK. Type is always "Text"; to send a typed variable | ||
| * (Boolean/Number/Object/List/Json) use --context-variables-json instead. | ||
| * | ||
| * Names pass through verbatim. The runtime distinguishes two namespaces by name | ||
| * shape: "$Context.<Name>" for linked context variables, bare "<developerName>" | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I went with
context-variables-jsoninstead ofcontext-variables-objectbecauseobjectalready means something in context variable land (a type of variable) so the name was a bit confusing. Plus, it is JSON.