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
}