import { CallExpressionShape, Plugin, type Record, PropertyAccessShape, type TemplateExpressionShape, } from '@servicenow/sdk-build-core' import { createVariablePropertyAccess, createScript, getTargetRecord, resolveVariableAccess, processCatalogCondition, processCatalogConditionsToShape, resolveCatalogReferences, convertToNumber, resolveAndValidateVariableId, } from './utils' import { NowIdShape } from '../now-id-plugin' import { APPLIES_TO_CATALOG_ITEM, getUITypeFromId, getUITypeId, getValueActionFromDb, getValueActionToDb, } from './utils' import { validateCatalogItemVariableSetExclusivity, validateUiPolicyActionMessage } from './service-catalog-diagnostics' // Define table names as constants since they're not exported from the core tables const CATALOG_UI_POLICY = 'catalog_ui_policy' const CATALOG_UI_POLICY_ACTION = 'catalog_ui_policy_action' const DEFAULT_SCRIPT = 'function onCondition() {\n\n}' /** * Helper function to convert ServiceNow string format ('true'/'false'/'ignore') to boolean | 'ignore' */ const getBooleanOrIgnore = (action: Record, field: string): boolean | 'ignore' | undefined => { const value = action.isRecord() ? action.asRecord() : action if (value.get(field)?.ifString()?.getValue() === 'ignore' || value.get(field)?.ifString()?.isEmpty()) { return 'ignore' } return value.get(field)?.toBoolean()?.getValue() } /** * Helper function to strip IO: prefix from variable names */ const stripIOPrefix = (value: string): string => { return value.startsWith('IO:') ? value.substring(3) : value } export const CatalogUiPolicyPlugin = Plugin.create({ name: 'CatalogUiPolicyPlugin', records: { [CATALOG_UI_POLICY]: { relationships: { [CATALOG_UI_POLICY_ACTION]: { via: 'ui_policy', descendant: true, }, }, async toShape(record, { descendants, database, transform }) { let processedConditions: string | TemplateExpressionShape | undefined // Handle catalog_condition - convert variables to PropertyAccessShape in template expression if (!record.get('catalog_conditions')?.ifString()?.isEmpty()) { processedConditions = processCatalogConditionsToShape(record, database) } const { catalogItemReference, variableSetReference } = resolveCatalogReferences(record, database) const actions = descendants.query(CATALOG_UI_POLICY_ACTION).map((action) => { const variableValue = action.get('catalog_variable')?.ifString()?.getValue() if (!variableValue) { return { success: false, } } const variableId = stripIOPrefix(variableValue) const varRecord = database.get('item_option_new', variableId) let variableName: PropertyAccessShape | string | undefined if (varRecord?.isRecord()) { const appliesToRecord = getTargetRecord(record, database) variableName = (appliesToRecord?.isRecord() && createVariablePropertyAccess(varRecord, record, appliesToRecord)) || variableId } else { variableName = variableId } return action.transform(({ $ }) => { const visible = getBooleanOrIgnore(action, 'visible') const readOnly = getBooleanOrIgnore(action, 'disabled') const mandatory = getBooleanOrIgnore(action, 'mandatory') const cleared = action.get('cleared')?.toBoolean()?.getValue() const variableMessage = action.get('field_message')?.ifString()?.getValue() const variableMessageType = action.get('field_message_type')?.ifString()?.getValue() const valueActionDb = action.get('value_action')?.ifString()?.getValue() const valueAction = valueActionDb ? getValueActionFromDb(valueActionDb) : undefined const order = convertToNumber(action.get('order'), 100) const value = action.get('value')?.ifString()?.getValue() const variable = action.get('variable')?.ifString()?.getValue() return { variableName: $.val(variableName), variable: $.val(variable).def(''), ...(visible !== 'ignore' && visible !== undefined && { visible: $.val(visible) }), ...(readOnly !== 'ignore' && readOnly !== undefined && { readOnly: $.val(readOnly) }), ...(mandatory !== 'ignore' && mandatory !== undefined && { mandatory: $.val(mandatory) }), ...(cleared && { cleared: $.val(cleared) }), ...(variableMessageType && variableMessageType !== 'none' && { variableMessageType: $.val(variableMessageType), }), ...(variableMessage && variableMessageType !== 'none' && { variableMessage: $.val(variableMessage), }), ...(valueAction && { valueAction: $.val(valueAction) }), ...(order && order !== 100 && { order: $.val(order) }), ...(value && { value: $.val(value) }), } }) }) const executeIfTrue = await createScript(record, record.get('script_true'), transform, 'script-true') const executeIfFalse = await createScript(record, record.get('script_false'), transform, 'script-false') return { success: true, value: new CallExpressionShape({ source: record, callee: 'CatalogUiPolicy', args: [ record.transform(({ $ }) => ({ $id: $.val(NowIdShape.from(record)), appliesTo: $.from('applies_to').def(APPLIES_TO_CATALOG_ITEM), catalogItem: $.val(catalogItemReference).def(''), variableSet: $.val(variableSetReference).def(''), shortDescription: $.from('short_description'), active: $.toBoolean().def(true), global: $.toBoolean().def(true), onLoad: $.from('on_load').toBoolean().def(true), reverseIfFalse: $.from('reverse_if_false').toBoolean().def(true), inherit: $.toBoolean().def(false), isolateScript: $.from('isolate_script').toBoolean().def(true), conditions: $.def(''), catalogCondition: $.val(processedConditions).def(''), appliesOnCatalogItemView: $.from('applies_catalog').toBoolean().def(true), appliesOnTargetRecord: $.from('applies_target_record').toBoolean().def(false), appliesOnCatalogTasks: $.from('applies_sc_task').toBoolean().def(false), appliesOnRequestedItems: $.from('applies_req_item').toBoolean().def(false), runScripts: $.from('run_scripts').toBoolean().def(false), executeIfTrue: $.val(executeIfTrue).def(DEFAULT_SCRIPT), executeIfFalse: $.val(executeIfFalse).def(DEFAULT_SCRIPT), description: $.from('description').def(''), modelId: $.from('model_id').def(''), modelTable: $.from('model_table').def(''), order: $.map((c) => convertToNumber(c, 100)).def(100), setValues: $.from('set_values').def(''), vaSupported: $.from('va_supported').toBoolean().def(true), view: $.from('view').def(''), publishedRef: $.from('published_ref').def(''), runScriptsInUiType: $.from('ui_type') .map((v) => v.ifString()?.isEmpty() ? undefined : getUITypeFromId(v.toNumber()?.getValue()) ) .def('all'), actions: $.val(actions.length > 0 ? actions : []).def([]), })), ], }), } }, }, [CATALOG_UI_POLICY_ACTION]: { coalesce: ['ui_policy', 'catalog_variable'], }, }, shapes: [ { shape: CallExpressionShape, fileTypes: ['fluent'], async toRecord(callExpression, { diagnostics, factory }) { if (callExpression.getCallee() !== 'CatalogUiPolicy') { return { success: false } } const arg = callExpression.getArgument(0).asObject() // Validate catalogItem and variableSet are mutually exclusive validateCatalogItemVariableSetExclusivity(arg, diagnostics, 'CatalogUiPolicy') const catalogConditionShape = arg.get('catalogCondition') let catalogConditionIO: string | TemplateExpressionShape | undefined const appliedTo: 'set' | 'item' = arg.get('appliesTo')?.toString()?.getValue() === 'set' ? 'set' : APPLIES_TO_CATALOG_ITEM if (catalogConditionShape.isDefined()) { catalogConditionIO = processCatalogCondition( catalogConditionShape, appliedTo, arg.get('variableSet', false), arg.get('catalogItem', false), diagnostics ) } // Create the Catalog UI Policy record const catalogUiPolicyRecord = await factory.createRecord({ source: callExpression, table: CATALOG_UI_POLICY, explicitId: arg.get('$id'), properties: arg.transform(({ $ }) => ({ catalog_item: $.from('catalogItem').def(''), variable_set: $.from('variableSet').def(''), applies_to: $.from('appliesTo').def(APPLIES_TO_CATALOG_ITEM), short_description: $.from('shortDescription'), active: $.def(true), global: $.def(true), on_load: $.from('onLoad').def(true), reverse_if_false: $.from('reverseIfFalse').def(true), inherit: $.def(false), isolate_script: $.from('isolateScript').def(true), conditions: $.def(''), catalog_conditions: $.val(catalogConditionIO).def(''), applies_catalog: $.from('appliesOnCatalogItemView').def(true), applies_target_record: $.from('appliesOnTargetRecord').def(false), applies_sc_task: $.from('appliesOnCatalogTasks').def(false), applies_req_item: $.from('appliesOnRequestedItems').def(false), run_scripts: $.from('runScripts').def(false), script_true: $.from('executeIfTrue').toCdata().def('function onCondition() {\n\n}'), script_false: $.from('executeIfFalse').toCdata().def('function onCondition() {\n\n}'), description: $.from('description').def(''), model_id: $.from('modelId').def(''), model_table: $.from('modelTable').def(''), order: $.def(100), set_values: $.from('setValues').def(''), published_ref: $.from('publishedRef').def(''), ui_type: $.from('runScriptsInUiType') .map((v) => (v.isString() && !v.isEmpty() ? getUITypeId(v.getValue()) : undefined)) .def(10), table: $.def(''), va_supported: $.from('vaSupported').def(true), view: $.def(''), })), }) // Process actions if they exist const actions = arg.get('actions')?.ifArray()?.getElements() ?? [] const actionRecords: Record[] = [] if (actions?.length > 0) { const variableNames = new Set() // Process each action sequentially for (const action of actions) { const actionObj = action.asObject() // Validate variableMessageType/variableMessage consistency validateUiPolicyActionMessage(actionObj, diagnostics) const variableNameShape = actionObj.get('variableName', false) // Resolve and validate catalog_variable const catalogVariable = resolveAndValidateVariableId( variableNameShape, appliedTo, arg.get('variableSet', false), arg.get('catalogItem', false), diagnostics ) // Extract variable name for duplicate checking let catalogVariableName: string | undefined if (variableNameShape?.is(PropertyAccessShape)) { const variableAccess = resolveVariableAccess(variableNameShape.as(PropertyAccessShape)) if (variableAccess) { catalogVariableName = variableAccess.variableName } else { catalogVariableName = variableNameShape.toString()?.getValue() } } else { catalogVariableName = variableNameShape?.toString()?.getValue() } if (catalogVariableName) { if (variableNames.has(catalogVariableName)) { diagnostics.error( variableNameShape, `Duplicate variableName '${catalogVariableName}' found in actions. Each action must have a unique variableName.` ) } variableNames.add(catalogVariableName) } actionRecords.push( await factory.createRecord({ source: action, // Use the action as source instead of callExpression table: CATALOG_UI_POLICY_ACTION, properties: actionObj.transform(({ $ }) => ({ ui_policy: $.val(catalogUiPolicyRecord.getId()), catalog_item: $.val(arg.get('catalogItem')?.toString()?.getValue() ?? ''), variable_set: $.val(arg.get('variableSet')?.toString()?.getValue() ?? ''), catalog_variable: $.val(catalogVariable).def(''), variable: $.from('variable').def(catalogVariableName ?? ''), visible: $.from('visible').def('ignore'), disabled: $.from('readOnly').def('ignore'), mandatory: $.from('mandatory').def('ignore'), order: $.def(100), cleared: $.from('cleared').toBoolean().def(false), field_message: $.from('variableMessage').def(''), field_message_type: $.from('variableMessageType').def('none'), value: $.from('value').def(''), value_action: $.from('valueAction') .map((v) => getValueActionToDb(v.toString().getValue())) .def('ignore'), })), }) ) } } return { success: true, value: catalogUiPolicyRecord.with(...actionRecords) } }, }, ], })