import { type Record, type Database, recordXml, type Shape } from '@servicenow/sdk-build-core' import type { Element } from 'xml-js' export const CHOICE_SET_VERSION = '4' export const LEGACY_CHOICE_SET_VERSION = '3' export const DEFAULT_INACTIVE = false export const DEFAULT_INACTIVE_ON_UPDATE = false export const CHOICE_PROPERTIES = [ 'label', 'value', 'sequence', 'dependent_value', 'hint', 'inactive', 'inactive_on_update', 'language', 'synonyms', ] as const /** * Generates the update name for both sys_choice and sys_choice_set records. * Both tables share the same naming convention: sys_choice__ */ export function getChoiceUpdateName(record: Record): string { return `sys_choice_${record.get('name').getValue()}_${record.get('element').getValue()}` } /** * Writes a sys_choice_set record element into the parent elements array. */ export function writeChoiceSetElement( parentElements: Element[], choiceSet: Record, tableName: string, elementName: string, config: { scope: string; scopeId: string } ) { const builder = recordXml(parentElements, 'sys_choice_set', choiceSet.getId().getValue(), { attr: { action: choiceSet.getAction() }, }) builder.field('element', elementName) builder.field('name', tableName) builder.field('sys_class_name', 'sys_choice_set') builder.addSysScope(config.scope, config.scopeId) builder.field('sys_package', config.scopeId, { display_value: config.scope, source: config.scopeId }) builder.field('sys_update_name', getChoiceUpdateName(choiceSet)) builder.field('sys_name', elementName) } /** * Writes a sys_choice child element into the parent elements array. */ export function writeChoiceElement(parentElements: Element[], choice: Record, tableName: string, elementName: string) { const builder = recordXml(parentElements, 'sys_choice', choice.getId().getValue(), { attr: { action: choice.getAction() }, }) builder.field('name', tableName) builder.field('element', elementName) for (const prop of CHOICE_PROPERTIES) { const val = choice.get(prop).ifDefined() if (val) { builder.field(prop, val) } } } const CHOICE_IDENTITY_FIELDS = ['name', 'element', 'value', 'language', 'dependent_value'] as const const MAX_QUERY_LENGTH = 4000 /** * Generates delete_multiple elements targeting all SDK-owned choices (both current * and removed), separated by ^NQ. Current choices use only identity fields (value, * language, dependent_value) since they will be re-created. Removed choices use all * record properties for precise targeting. Splits into multiple elements if the * query exceeds MAX_QUERY_LENGTH. */ export function writeDeleteMultipleElement( parentElements: Element[], currentChoices: Record[], database: Database, tableName: string, elementName: string ): void { const recordQueries: string[] = [] for (const choice of currentChoices) { const queryParts: string[] = [] for (const field of CHOICE_IDENTITY_FIELDS) { const val = choice.get(field).ifDefined()?.getValue() if (val !== undefined && val !== '') { queryParts.push(`${field}=${val}`) } } if (queryParts.length > 0) { recordQueries.push(queryParts.join('^')) } } const removedRecords = database .query('sys_choice') .filter( (c) => c.getAction() === 'DELETE' && c.get('name').ifString()?.getValue() === tableName && c.get('element').ifString()?.getValue() === elementName ) for (const record of removedRecords) { const queryParts: string[] = [] for (const field of record.keys()) { const val = record.get(field).ifDefined()?.getValue() if (val !== undefined && val !== '' && val !== 'NULL') { queryParts.push(`${field}=${val}`) } } if (queryParts.length > 0) { recordQueries.push(queryParts.join('^')) } } let currentBatch: string[] = [] let currentLength = 0 for (const query of recordQueries) { const separatorLength = currentBatch.length > 0 ? 3 : 0 // ^NQ if (currentLength + separatorLength + query.length > MAX_QUERY_LENGTH && currentBatch.length > 0) { parentElements.push({ type: 'element', name: 'sys_choice', attributes: { action: 'delete_multiple', query: currentBatch.join('^NQ') }, }) currentBatch = [] currentLength = 0 } currentBatch.push(query) currentLength += (currentBatch.length > 1 ? 3 : 0) + query.length } if (currentBatch.length > 0) { parentElements.push({ type: 'element', name: 'sys_choice', attributes: { action: 'delete_multiple', query: currentBatch.join('^NQ') }, }) } } export const choiceAliases = { dependentValue: ['dependent_value'], inactiveOnUpdate: ['inactive_on_update'], } const sequenceMapper = (sequence: Shape) => { if (sequence.isNumber()) { return sequence } else if (sequence.isString()) { const sequenceVal = sequence.asString().getValue().trim() return sequenceVal !== '' && !isNaN(Number(sequenceVal)) ? Number(sequenceVal) : undefined } return undefined } const depValMapper = (depVal: Shape) => { if (depVal.isNumber()) { return depVal.asNumber().getValue() } else if (depVal.isString()) { const depValue = depVal.asString().getValue().trim() return (isNaN(Number(depValue)) ? depValue : Number(depValue)) || undefined } return undefined } function transformChoice(record: Record, defaultLanguage: string) { return record .transform(({ $ }) => ({ label: $, sequence: $.map(sequenceMapper), dependentValue: $.from('dependent_value').map(depValMapper), inactive: $.toBoolean().def(DEFAULT_INACTIVE), inactiveOnUpdate: $.from('inactive_on_update').toBoolean().def(DEFAULT_INACTIVE_ON_UPDATE), hint: $.def(''), synonyms: $.map((s) => { const str = s.ifString()?.getValue() ?? '' return str ? str.split(',') : undefined }), language: $.def(defaultLanguage), })) .withAliasedKeys(choiceAliases) } export function generateChoices(choices: Record[], defaultLanguage: string): globalThis.Record { const choicesByValue = new Map() for (const choiceRecord of choices) { const value = choiceRecord.get('value').asString().getValue() if (value !== undefined) { const group = choicesByValue.get(value) group ? group.push(choiceRecord) : choicesByValue.set(value, [choiceRecord]) } } const result: globalThis.Record = {} for (const [value, valueRecords] of choicesByValue) { result[value] = valueRecords.length === 1 ? transformChoice(valueRecords[0]!, defaultLanguage) : valueRecords.map((r) => transformChoice(r, defaultLanguage)) } return result }