import { CallExpressionShape, type Record, type Database, type Logger, type Shape } from '@servicenow/sdk-build-core' import type { ExtendedChoices } from '@servicenow/sdk-core/service-catalog' import { VARIABLE_TYPE_TO_NAME, VariableTypeName } from './variable-helper' import { convertToNumber, createVariablePropertyAccess } from './utils' import { buildVariableTransform } from './variables-transform' interface DependentQuestionResult { value: string | Shape /** If true, the referenced variable was deleted and dynamic default config should be cleared */ shouldClearDynamicDefault: boolean } /** * Finds the container record (sc_cat_item, sc_cat_item_producer, or item_option_new_set) that owns * a variable record, so createVariablePropertyAccess can build `container.variables.name`. */ function findVariableContainer(variableRecord: Record, descendants: Database): Record | undefined { const variableSetShape = variableRecord.get('variable_set') const variableSetId = variableSetShape?.ifString()?.getValue() || variableSetShape?.ifRecord()?.getId()?.getValue() if (variableSetId) { return descendants.get('item_option_new_set', variableSetId) } const catItemShape = variableRecord.get('cat_item') const catItemId = catItemShape?.ifString()?.getValue() || catItemShape?.ifRecord()?.getId()?.getValue() if (catItemId) { return descendants.get('sc_cat_item', catItemId) ?? descendants.get('sc_cat_item_producer', catItemId) } return undefined } /** * Reconstructs the dependentQuestion form (string, object-ref, or dot-walk) for round-trip fidelity. * The reference is always reconstructed structurally from the referenced variable's own AST node — * a dot-walk expression if it lives in a container, otherwise its name — rather than preserving * whatever string the developer originally typed. This keeps the resolution logic simple and * consistent with how other plugins (e.g. catalog UI policy) resolve variable references. */ function generateDependentQuestionValue( source: Record, dynamicValueFieldSysId: string, descendants: Database ): DependentQuestionResult { const referencedVariable = dynamicValueFieldSysId && descendants.get('item_option_new', dynamicValueFieldSysId) if (!referencedVariable) { // dynamic_value_field is a non-empty sys_id that isn't tracked by this project's descendants — // e.g. it belongs to a VariableSet/variable outside this app's scope (referenced by raw sys_id // rather than built by this project). We have no way to tell that apart from a genuinely deleted // variable using only this sys_id, so preserve it verbatim rather than risk silently dropping a // valid out-of-scope reference. if (dynamicValueFieldSysId) { return { value: dynamicValueFieldSysId, shouldClearDynamicDefault: false } } // dynamic_value_field is genuinely empty (cleared on the platform or never set) — clear the whole // dynamic default config so useDynamicDefault/dotWalkPath don't linger without a valid source. return { value: '', shouldClearDynamicDefault: true } } const referencedVariableName = referencedVariable.get('name')?.ifString()?.getValue() ?? dynamicValueFieldSysId const referencedContainer = findVariableContainer(referencedVariable, descendants) // No AST to walk (e.g. record came from a real instance, never authored in Fluent this build) — fall back to the name. const reference = (referencedContainer && createVariablePropertyAccess(referencedVariable, source, referencedContainer)) || referencedVariableName return { value: reference, shouldClearDynamicDefault: false } } /** * Converts a ServiceNow variable record to a CallExpressionShape for code generation. * This function queries the database for variable data and delegates transformation to buildVariableTransform. */ export function variableToCallExpression(variable: Record, descendants: Database, logger: Logger): CallExpressionShape { const typeCode = variable.get('type').ifString()?.getValue() || '' const callExpression = VARIABLE_TYPE_TO_NAME[typeCode] if (!callExpression) { throw new Error(`Unknown variable type: ${typeCode}`) } const choicesData: ExtendedChoices = {} if (callExpression === VariableTypeName.MULTIPLE_CHOICE || callExpression === VariableTypeName.SELECT_BOX) { const choices = descendants.query('question_choice', { question: variable.getId()?.getValue(), }) choices.forEach((choice) => { const choicePriceDetails = descendants.query('fx_price', { id: choice.getId()?.getValue(), }) const choiceValue = choice.get('value')?.ifString()?.getValue() ?? choice.get('value')?.ifNumber()?.getValue() const pricingDetailsArray = choicePriceDetails.map((price) => { const amount = convertToNumber(price.get('amount'), 0) const currencyType = price.get('currency').ifString()?.getValue() || '' const field = price.get('field').ifString()?.getValue() || '' return { amount, currencyType, field } }) if (choiceValue === undefined) { return } choicesData[choiceValue] = { label: choice.get('text').ifString()?.getValue() || '', inactive: choice.get('inactive').toBoolean().getValue() || false, sequence: choice.get('order').ifString()?.isEmpty() || choice.get('order').isUndefined() ? 0 : choice.get('order').toNumber().getValue(), ...(pricingDetailsArray.length > 0 && { pricingDetails: pricingDetailsArray }), } }) } const prices = descendants.query('fx_price', { id: variable.getId()?.getValue(), }) let varPricingDetails: { amount: number; currencyType: string; field: string }[] = [] if (callExpression === VariableTypeName.CHECKBOX) { varPricingDetails = prices.map((price) => { const amount = convertToNumber(price.get('amount'), 0) const currencyType = price.get('currency').ifString()?.getValue() || '' const field = price.get('field').ifString()?.getValue() || '' return { amount, currencyType, field } }) } // Determine discriminated union types before transform let qualifierType: 'dynamic' | 'advanced' | 'simple' = 'simple' if ( callExpression === VariableTypeName.REFERENCE || callExpression === VariableTypeName.REQUESTED_FOR || callExpression === VariableTypeName.LIST_COLLECTOR ) { const useRefQual = variable.get('use_reference_qualifier')?.ifString()?.getValue() if (useRefQual === 'dynamic') { qualifierType = 'dynamic' } else if (useRefQual === 'advanced') { qualifierType = 'advanced' } } let lookupSource: 'choices' | 'table' = 'table' if ( callExpression === VariableTypeName.LOOKUP_SELECT_BOX || callExpression === VariableTypeName.LOOKUP_MULTIPLE_CHOICE ) { const lookupSourceValue = variable.get('lookup_source')?.ifString()?.getValue() if (lookupSourceValue === 'choices') { lookupSource = 'choices' } } const dynamicValueFieldShape = variable.get('dynamic_value_field') const dynamicValueFieldSysId = dynamicValueFieldShape?.ifString()?.getValue() ?? dynamicValueFieldShape?.ifRecord()?.getId()?.getValue() ?? '' const dependentQuestionResult = generateDependentQuestionValue(variable, dynamicValueFieldSysId, descendants) return new CallExpressionShape({ source: variable, callee: callExpression, args: [ variable.transform(({ $ }) => buildVariableTransform( callExpression, $, { choicesData, varPricingDetails, qualifierType, lookupSource, dependentQuestionValue: dependentQuestionResult.value, shouldClearDynamicDefault: dependentQuestionResult.shouldClearDynamicDefault, }, logger ) ), ], }) }