import type { Logger, Shape, ShapeTransform } from '@servicenow/sdk-build-core' import type { ExtendedChoices, PricingDetail } from '@servicenow/sdk-core/service-catalog' import { VariableTypeName } from './variable-helper' import { parseString, getVisibilityFromId } from './utils' /** * All variable transformation functions that map database fields to TypeScript type definitions. * Each transform function respects the exact type definition from packages/core/src/service-catalog/types/Variables/ */ // Base transformer that applies to ALL variable types (BaseVariableConfig fields) // Omits dotWalkPath/dependentQuestion when shouldClearDynamicDefault (referenced variable was deleted). // useDynamicDefault is always emitted from use_dynamic_default — it is not exclusive to dependentQuestion // on the platform (e.g. dynamic_default_value-driven defaults also use it), so it is not cleared here. export const createBaseTransform = ( $: ShapeTransform, dependentQuestionValue?: string | Shape, shouldClearDynamicDefault?: boolean ) => { const baseProps = { active: $.from('active').toBoolean().def(true), question: $.from('question_text').def(''), order: $.from('order') .map((c: Shape) => (c.ifString()?.isEmpty() || c.isUndefined() ? undefined : c.toNumber().getValue())) .def(0), disableInitialSlotFill: $.from('disable_initial_slot_fill').toBoolean().def(false), conversationalLabel: $.from('conversational_label').def(''), tooltip: $.from('tooltip').def(''), exampleText: $.from('example_text').def(''), showHelp: $.from('show_help').toBoolean().def(false), helpTag: $.from('help_tag').def('More information'), helpText: $.from('help_text').def(''), instructions: $.from('instructions').def(''), width: $.from('variable_width') .map((c: Shape) => (c.ifString()?.isEmpty() || c.isUndefined() ? undefined : c.toNumber().getValue())) .def(''), attributes: $.from('attributes').def(''), defaultValue: $.from('default_value').def(''), readRoles: $.from('read_roles') .map((v: Shape) => parseString(v)) .def([]), writeRoles: $.from('write_roles') .map((v: Shape) => parseString(v)) .def([]), createRoles: $.from('create_roles') .map((v: Shape) => parseString(v)) .def([]), visibleStandalone: $.from('visible_standalone').toBoolean().def(true), visibleSummary: $.from('visible_summary').toBoolean().def(true), visibleGuide: $.from('visible_guide').toBoolean().def(true), visibleBundle: $.from('visible_bundle').toBoolean().def(true), removeFromConversationalInterfaces: $.from('not_available_conversation').toBoolean().def(false), mapToField: $.from('map_to_field').toBoolean().def(false), field: $.from('map_to_field', 'field') .map((mapToField: Shape, field: Shape) => { return mapToField.toBoolean().getValue() ? field.toString().getValue() : undefined }) .def(''), alwaysExpand: $.from('show_help_on_load').toBoolean().def(false), description: $.def(''), global: $.toBoolean().def(false), deliveryPlan: $.from('delivery_plan').def(''), visibility: $.from('visibility') .map((v: Shape) => getVisibilityFromId(v.ifString()?.isEmpty() || v.isUndefined() ? 1 : v.toNumber().getValue()) ) .def('Always'), category: $.from('category').def(''), pricingImplications: $.from('pricing_implications').toBoolean().def(false), useDynamicDefault: $.from('use_dynamic_default').toBoolean().def(false), unique: $.from('unique').toBoolean().def(false), readScript: $.from('read_script').def(''), postInsertScript: $.from('post_insert_script').def(''), } if (shouldClearDynamicDefault) { return baseProps } return { ...baseProps, dotWalkPath: $.from('dynamic_value_dot_walk_path').def(''), // Pre-resolved by the caller for round-trip fidelity. dependentQuestion: $.val(dependentQuestionValue).def(''), } } // Interactive fields (VariableConfig extends BaseVariableConfig) export const createInteractiveTransform = ($: ShapeTransform) => ({ mandatory: $.from('mandatory').toBoolean().def(false), readOnly: $.from('read_only').toBoolean().def(false), hidden: $.from('hidden').toBoolean().def(false), }) // Layout fields for container variables export const createLayoutTransform = ($: ShapeTransform) => ({ layout: $.from('layout') .map((c: Shape) => (c.ifString()?.isEmpty() || c.isUndefined() ? undefined : c.toString().getValue())) .def('normal'), displayTitle: $.from('display_title').toBoolean().def(false), }) // SelectBox-specific fields export const createSelectBoxTransform = ($: ShapeTransform, choicesData: ExtendedChoices) => ({ choices: $.val(choicesData).def({}), choiceTable: $.from('choice_table').def(''), choiceField: $.from('choice_field').def(''), includeNone: $.from('include_none').toBoolean().def(false), uniqueValuesOnly: $.from('lookup_unique').toBoolean().def(false), }) // MultipleChoice-specific fields export const createMultipleChoiceTransform = ($: ShapeTransform, choicesData: ExtendedChoices) => ({ choices: $.val(choicesData).def({}), choiceDirection: $.from('choice_direction') .map((c: Shape) => (c.ifString()?.isEmpty() ? undefined : c)) .def('down'), includeNone: $.from('include_none').toBoolean().def(false), doNotSelectFirstChoice: $.from('do_not_select_first').toBoolean().def(false), }) // NumericScale-specific fields export const createNumericScaleTransform = ($: ShapeTransform) => ({ scaleMin: $.from('scale_min') .map((c: Shape) => (c.ifString()?.isEmpty() || c.isUndefined() ? undefined : c.toNumber().getValue())) .def(0), scaleMax: $.from('scale_max') .map((c: Shape) => (c.ifString()?.isEmpty() || c.isUndefined() ? undefined : c.toNumber().getValue())) .def(5), doNotSelectFirstChoice: $.from('do_not_select_first').toBoolean().def(false), }) // Reference fields - respects discriminated union export const createReferenceTransform = ($: ShapeTransform, qualifierType: 'simple' | 'advanced' | 'dynamic') => { const baseTransform = { referenceTable: $.from('reference').def(''), deleteRoles: $.from('delete_roles') .map((v: Shape) => parseString(v)) .def([]), } if (qualifierType === 'dynamic') { return { ...baseTransform, useReferenceQualifier: $.val('dynamic').def('simple'), dynamicRefQual: $.from('dynamic_ref_qual').def(''), } } else if (qualifierType === 'advanced') { return { ...baseTransform, useReferenceQualifier: $.val('advanced').def('simple'), referenceQual: $.from('reference_qual').def(''), } } else { // simple - no qualifier return { ...baseTransform, referenceQualCondition: $.from('reference_qual_condition').def(''), } } } // RequestedFor fields - similar to Reference but without referenceTable export const createRequestedForTransform = ($: ShapeTransform, qualifierType: 'simple' | 'advanced' | 'dynamic') => { const baseTransform = { enableAlsoRequestFor: $.from('enable_also_request_for').toBoolean().def(false), rolesToUseAlsoRequestFor: $.from('roles_to_use_also_request_for') .map((v: Shape) => parseString(v)) .def([]), } if (qualifierType === 'dynamic') { return { ...baseTransform, useReferenceQualifier: $.val('dynamic').def('dynamic'), dynamicRefQual: $.from('dynamic_ref_qual').def(''), } } else if (qualifierType === 'advanced') { return { ...baseTransform, useReferenceQualifier: $.val('advanced').def('advanced'), referenceQual: $.from('reference_qual').def(''), } } else { // simple - no qualifier return { ...baseTransform, referenceQualCondition: $.from('reference_qual_condition').def(''), } } } // Lookup fields - respects discriminated union export const createLookupTransform = ($: ShapeTransform, lookupSource: 'choices' | 'table') => { const baseTransform = { choiceDirection: $.from('choice_direction') .map((c: Shape) => c.asString().isEmpty() || c.asString().isUndefined() ? undefined : c.asString().getValue() ) .def('down'), includeNone: $.from('include_none').toBoolean().def(false), uniqueValuesOnly: $.from('lookup_unique').toBoolean().def(false), referenceQual: $.from('reference_qual').def(''), } if (lookupSource === 'choices') { return { ...baseTransform, lookupSource: $.val('choices'), choiceTable: $.from('choice_table').def(''), choiceField: $.from('choice_field').def(''), choicesDependOn: $.from('lookup_dependent_question').def(''), } } else { return { ...baseTransform, lookupFromTable: $.from('lookup_table').def(''), lookupValueField: $.from('lookup_value').def(''), lookupLabelFields: $.from('lookup_label') .map((v: Shape) => parseString(v)) .def([]), lookupPriceField: $.from('lookup_price').def(''), lookupRecurringPriceField: $.from('rec_lookup_price').def(''), } } } export const createCustomTransform = ($: ShapeTransform) => ({ macro: $.from('macro').def(''), summaryMacro: $.from('summary_macro').def(''), widget: $.from('sp_widget').def(''), macroponent: $.from('macroponent').def(''), topicBlock: $.from('topic_block').def(''), }) // Note: Simple single-use transforms have been inlined directly into the switch cases below. // Only frequently used and complex transforms are kept as separate functions. /** * Build transformation based on variable type. * Each case maps only the fields defined in that variable's TypeScript type definition. */ export function buildVariableTransform( variableType: string, $: ShapeTransform, params: { choicesData: ExtendedChoices varPricingDetails: PricingDetail[] qualifierType?: 'simple' | 'advanced' | 'dynamic' lookupSource?: 'choices' | 'table' dependentQuestionValue?: string | Shape shouldClearDynamicDefault?: boolean }, logger: Logger ) { // Handle minimal variables that don't extend BaseVariableConfig if ( variableType === VariableTypeName.BREAK || variableType === VariableTypeName.CONTAINER_END || variableType === VariableTypeName.CONTAINER_SPLIT ) { return { active: $.from('active').toBoolean().def(true), order: $.from('order') .map((c: Shape) => (c.ifString()?.isEmpty() || c.isUndefined() ? undefined : c.toNumber().getValue())) .def(0), disableInitialSlotFill: $.from('disable_initial_slot_fill').toBoolean().def(false), } } // Start with base fields (all other variables have these) // Note: Using Record type here because transform properties are dynamically added based on variable type // biome-ignore lint/suspicious/noExplicitAny: Dynamic transform object with varying shapes based on variable type let transform: Record = { ...createBaseTransform($, params.dependentQuestionValue, params.shouldClearDynamicDefault), } // Add type-specific fields switch (variableType) { case VariableTypeName.REFERENCE: transform = { ...transform, ...createInteractiveTransform($), ...createReferenceTransform($, params.qualifierType || 'simple'), } break case VariableTypeName.REQUESTED_FOR: transform = { ...transform, ...createInteractiveTransform($), ...createRequestedForTransform($, params.qualifierType || 'simple'), } break case VariableTypeName.LOOKUP_SELECT_BOX: case VariableTypeName.LOOKUP_MULTIPLE_CHOICE: transform = { ...transform, ...createInteractiveTransform($), ...createLookupTransform($, params.lookupSource || 'table'), } break case VariableTypeName.LIST_COLLECTOR: transform = { ...transform, ...createInteractiveTransform($), ...{ listTable: $.from('list_table').def(''), referenceQual: $.from('reference_qual').def(''), }, } break case VariableTypeName.SELECT_BOX: transform = { ...transform, ...createInteractiveTransform($), ...createSelectBoxTransform($, params.choicesData), } break case VariableTypeName.MULTIPLE_CHOICE: transform = { ...transform, ...createInteractiveTransform($), ...createMultipleChoiceTransform($, params.choicesData), } break case VariableTypeName.NUMERIC_SCALE: transform = { ...transform, ...createInteractiveTransform($), ...createNumericScaleTransform($), } break case VariableTypeName.CUSTOM: case VariableTypeName.CUSTOM_WITH_LABEL: transform = { ...transform, ...createCustomTransform($), } break case VariableTypeName.MASKED: transform = { ...transform, ...createInteractiveTransform($), ...{ useConfirmation: $.from('mask_use_confirmation').toBoolean().def(false), useEncryption: $.from('mask_use_encryption').toBoolean().def(false), }, } break case VariableTypeName.HTML: transform = { ...transform, ...createInteractiveTransform($), ...{ defaultHTML: $.from('default_html_value').def(''), }, } break case VariableTypeName.RICH_TEXT_LABEL: transform = { ...transform, ...{ richText: $.from('rich_text').def(''), question: $.val(undefined).def(''), // Override base transform - question field should not exist for RichTextLabel defaultHTML: $.val(undefined).def(''), }, } break case VariableTypeName.SINGLE_LINE_TEXT: case VariableTypeName.WIDE_SINGLE_LINE_TEXT: transform = { ...transform, ...createInteractiveTransform($), ...{ validateRegex: $.from('validate_regex').def(''), }, } break case VariableTypeName.MULTI_LINE_TEXT: case VariableTypeName.DATE: case VariableTypeName.DATE_TIME: case VariableTypeName.DURATION: case VariableTypeName.EMAIL: case VariableTypeName.URL: case VariableTypeName.IP_ADDRESS: case VariableTypeName.ATTACHMENT: // Standard interactive variables with BASE + INTERACTIVE fields only transform = { ...transform, ...createInteractiveTransform($), } break case VariableTypeName.YES_NO: transform = { ...transform, ...createInteractiveTransform($), ...{ includeNone: $.from('include_none').toBoolean().def(false), }, } break case VariableTypeName.UI_PAGE: transform = { ...transform, ...{ uiPage: $.from('ui_page').def(''), }, } break case VariableTypeName.CHECKBOX: transform = { ...transform, ...{ selectionRequired: $.from('mandatory').toBoolean().def(false), readOnly: $.from('read_only').toBoolean().def(false), hidden: $.from('hidden').toBoolean().def(false), pricingDetails: $.val(params.varPricingDetails).def([]), }, } break case VariableTypeName.CONTAINER_START: transform = { ...transform, ...createLayoutTransform($), } break case VariableTypeName.LABEL: // Non-interactive variable - only has base fields break default: logger.warn(`Unknown variable type encountered: ${variableType}. Using default transform.`) transform = { ...transform, ...createInteractiveTransform($), } break } return transform }