import { CallExpressionShape, isSNScope, type Diagnostics, type Factory, ObjectShape, Plugin, type Record, Shape, type ShapeTransform, } from '@servicenow/sdk-build-core' import { BooleanColumn, choiceDropdown, type choiceDropdownType, GenericColumn } from '@servicenow/sdk-core/runtime/db' import { getChoiceRecords, getLabelForDefaultLanguage } from './column/column-to-record' import { COLUMN_API_TO_TYPE, COLUMN_TYPE_TO_API, DEFAULT_COLUMN_CALCULATION } from './column/column-helper' import { ModuleFunctionShape } from './server-module-plugin' import { generateDeprecatedDiagnostics } from './utils' import { choiceAliases, generateChoices } from './choice-set-utils' import * as _ from 'lodash' // BCP 47 language tag pattern: 2-3 letter base language with optional subtags (e.g., 'en', 'zh-Hans', 'en-US') export const BCP47_LANGUAGE_TAG_PATTERN = /^[a-z]{2,3}(-[a-zA-Z0-9]{2,8})*$/ const documentationAliases = { urlTarget: ['url_target'], } const ACRONYMS = new Set([ 'css', 'id', 'pid', 'url', 'uri', 'xml', 'html', 'ip', 'ci', 'hr', 'cmdb', 'cpu', 'ecc', 'ldap', 'ola', 'olap', 'rfc', 'sla', 'sql', 'ui', 'uid', 'unix', 'ups', 'vpn', ]) const PLURAL_EQUALS_LABEL = [ 's', 'ed', 'by', 'Active', ')', ' to', ' with', ' for', ' in', ' out', ' use', 'Data', ' data', ] const PLURAL_SPECIAL_CASE_Y_TO_IES = ['ry', 'ty', 'dy', 'cy', 'fy', 'ly', 'ny'] const PLURAL_SPECIAL_CASE_ADD_ES = ['x', 'ch', 'sh', 'z'] const columnAliases = { readOnly: ['read_only'], functionDefinition: ['function_definition'], tableReference: ['table_reference'], elementReference: ['element_reference'], spellCheck: ['spell_check'], xmlView: ['xml_view'], textIndex: ['text_index'], dynamicValueDefinitions: ['dynamic_value_definitions'], referenceFloats: ['reference_floats'], dynamicCreationScript: ['dynamic_creation_script'], dynamicCreation: ['dyamic_creation'], // maintaining backwards compatibility with typo referenceKey: ['reference_key'], referenceQual: ['reference_qual'], columnType: ['column_type'], virtualType: ['virtual_type'], useReferenceQualifier: ['use_reference_qualifier'], dynamicRefQual: ['dynamic_ref_qual'], } const calculationAliases = { calculatedValue: ['calculated_value'], columnName: ['column_name'], } // Column types whose `dependent` value names a field in the table rather than a choice-dependency. // For these, the `dependent_on_field` mirrors `dependent` and `use_dependent_field` is enabled automatically. const DEPENDENT_FIELD_COLUMN_TYPES = new Set(['document_id', 'field_list', 'template_value']) export const ColumnPlugin = Plugin.create({ name: 'ColumnPlugin', shapes: [ { shape: CallExpressionShape, fileTypes: ['fluent'], async toRecord(callExpression, { factory, diagnostics, config }) { const callee = callExpression.getCallee() if (!(callee in COLUMN_API_TO_TYPE) && !(callee === GenericColumn.name)) { return { success: false } } const column = callExpression.getArgument(0).asObject().withAliasedKeys(columnAliases) generateDeprecatedDiagnostics(column, diagnostics) const tableName = column.get('table').ifString()?.getValue() const columnName = column.get('name').ifString()?.getValue() if (!tableName || !columnName) { return { success: false } } const documentationRecords = await labelShapeToDocumentation( column.get('label'), tableName, columnName, config.defaultLanguage, factory, diagnostics ) const choiceRecords = await getChoiceRecords( callExpression, choiceAliases, factory, diagnostics, config.defaultLanguage ) const choiceSetRecords: Record[] = [] if (choiceRecords.length > 0) { choiceSetRecords.push( await factory.createRecord({ source: callExpression, table: 'sys_choice_set', properties: { name: tableName, element: columnName, }, }) ) } const columnType = COLUMN_API_TO_TYPE[callee] ?? column.get('columnType').asString().getValue() const isColumnDependentFieldType = DEPENDENT_FIELD_COLUMN_TYPES.has(columnType) const dynamicRefQualShape = column.get('dynamicRefQual') const dynamicRefQualRef = dynamicRefQualShape.isString() && dynamicRefQualShape.getValue() !== '' ? await factory.createReference({ source: dynamicRefQualShape, table: 'sys_filter_option_dynamic', guid: dynamicRefQualShape, }) : dynamicRefQualShape.ifDefined() const mtomValue = column.get('mtom').ifString()?.ifNotEmpty() if (mtomValue && !isSNScope(config.scope)) { diagnostics.warn( mtomValue, `'mtom' is reserved for ServiceNow internal use and will be ignored. Use the sys_m2m table to establish many-to-many relationships.` ) } return { success: true, value: ( await factory.createRecord({ source: callExpression, table: 'sys_dictionary', properties: column.transform(({ $ }) => ({ name: $.val(tableName), element: $.val(columnName), internal_type: $.from('isFullUTF8') .map((isFullUTF8) => isFullUTF8?.isBoolean() && isFullUTF8.asBoolean().getValue() && columnType === 'string' ? 'string_full_utf8' : columnType ) .def(columnType), active: $.toBoolean().def(true), array: $.toBoolean().def(false), attributes: $.from('attributes', 'scale', 'script') .map((attributes, scale, script) => { const attrs = attributes?.isObject() ? attributes.asObject().getValue() : {} // Add special parameters to attributes if (scale?.isNumber()) { attrs['scale'] = scale.asNumber().getValue() } if (script?.isString()) { attrs['script'] = script.asString().getValue() } return Object.entries(attrs) .map(([key, value]) => `${key}=${value}`) .join(',') }) .def(''), audit: $.toBoolean().def(false), reference_cascade_rule: $.from('cascadeRule'), default_value: $.from('default'), display: $.toBoolean().def(false), dynamic_creation: $.from('dynamicCreation').toBoolean(), dynamic_creation_script: $.from('dynamicCreationScript'), reference_floats: $.from('referenceFloats'), reference_key: $.from('referenceKey'), mtom: $.from('mtom'), reference_qual: $.from('referenceQual'), calculation: $.from('dynamicValueDefinitions') .map((dynamicValueDefinitions) => { if ( dynamicValueDefinitions.ifObject()?.get('type').ifString()?.getValue() !== 'calculated_value' ) { return undefined } const dynamicValueObject = dynamicValueDefinitions .asObject() .withAliasedKeys(calculationAliases) generateDeprecatedDiagnostics(dynamicValueObject, diagnostics) const calculatedValue = dynamicValueObject.get('calculatedValue') if (calculatedValue.isUnresolved()) { diagnostics.error( calculatedValue.getOriginalNode(), `Unable to resolve the script reference, ensure the imported module is within the ${config.serverModulesDir} directory.` ) } return ( calculatedValue .if(ModuleFunctionShape) ?.toString((n) => `${n}({{PARAMS}})`, ['current']) ?? calculatedValue ) }) .toCdata(), choice: $.from('dropdown').map((d) => { if (!d.isString()) { return undefined } const idx = choiceDropdown.indexOf(d.getValue() as choiceDropdownType) // Index 0 ('none') is the platform default — leave undefined so // the install XML emits empty , matching stock records. return idx > 0 ? idx : undefined }), choice_table: $.from('dynamicValueDefinitions').map((dynamicValueDefinitions) => dynamicValueDefinitions.ifObject()?.get('type').ifString()?.getValue() === 'choices_from_other_table' ? dynamicValueDefinitions.asObject().get('table').ifString() : undefined ), virtual: $.from('dynamicValueDefinitions', 'formula') .map((dynamicValueDefinitions, formula) => { const hasFormula = formula.isString() && formula.getValue() !== '' if (hasFormula) { return true } const calculationType = dynamicValueDefinitions .ifObject() ?.get('type') .ifString() ?.getValue() if (!calculationType) { return undefined } return calculationType === 'calculated_value' }) .toBoolean(), choice_field: $.from('dynamicValueDefinitions').map((dynamicValueDefinitions) => dynamicValueDefinitions.ifObject()?.get('type').ifString()?.getValue() === 'choices_from_other_table' ? dynamicValueDefinitions.asObject().get('field').ifString() : undefined ), dependent: $.from('dependent', 'dynamicValueDefinitions').map( (dependent, dynamicValueDefinitions) => { // New direct approach (DocumentIdColumn, TemplateValueColumn, FieldListColumn) if (dependent?.isString()) { return dependent.asString().getValue() } if (dependent?.isObject()) { diagnostics.warn( dependent.getOriginalNode(), `Passing a TableNameColumn object to 'dependent' is deprecated and will not work as expected. Pass the field name as a string instead.` ) } // Legacy approach via dynamicValueDefinitions if ( dynamicValueDefinitions?.ifObject()?.get('type').ifString()?.getValue() === 'dependent_field' ) { const dynamicValueObject = dynamicValueDefinitions .asObject() .withAliasedKeys(calculationAliases) generateDeprecatedDiagnostics(dynamicValueObject, diagnostics) diagnostics.warn( dynamicValueDefinitions.getOriginalNode(), `Using 'dynamicValueDefinitions: { type: "dependent_field" }' is deprecated. ` + `Use 'dependent: "field_name"' directly on the column instead.` ) return dynamicValueObject.get('columnName').ifString()?.getValue() } return undefined } ), dependent_on_field: $.from('dependent', 'dynamicValueDefinitions').map( (dependent, dynamicValueDefinitions) => { if (isColumnDependentFieldType) { const direct = dependent.ifString()?.ifNotEmpty() if (direct) { return direct } } if ( dynamicValueDefinitions.ifObject()?.get('type').ifString()?.getValue() !== 'dependent_field' ) { return undefined } const dynamicValueObject = dynamicValueDefinitions .asObject() .withAliasedKeys(calculationAliases) generateDeprecatedDiagnostics(dynamicValueObject, diagnostics) return dynamicValueObject.get('columnName').ifString() } ), use_dependent_field: $.from('dependent', 'dynamicValueDefinitions') .map((dependent, dynamicValueDefinitions) => { if (isColumnDependentFieldType && dependent.ifString()?.ifNotEmpty()) { return true } return ( dynamicValueDefinitions.ifObject()?.get('type').ifString()?.getValue() === 'dependent_field' ) }) .def(false), element_reference: $.from('elementReference').toBoolean().def(false), function_definition: $.from('functionDefinition'), function_field: $.from('functionDefinition') .map((f) => f.isString() && f.getValue() !== '') .def(false), column_label: $.val( getLabelForDefaultLanguage(documentationRecords, config.defaultLanguage) ?? generateLabel(columnName) ), mandatory: $.toBoolean().def(false), max_length: $.from('maxLength').map( (maxLength) => maxLength.ifNumber()?.getValue() ?? maxLength.ifString()?.getValue() ), primary: $.toBoolean().def(false), read_only: $.from('readOnly', 'readOnlyOption') .map((readOnly, readOnlyOption) => { const readOnlyValue = readOnly.ifBoolean()?.getValue() const readOnlyOptionValue = readOnlyOption.ifString()?.getValue() if (readOnlyValue === false && readOnlyOptionValue) { diagnostics.error( readOnly, `readOnly cannot be false with readOnlyOption ${readOnlyOptionValue}.` ) } if (readOnlyValue === true && readOnlyOptionValue) { diagnostics.hint( readOnly, `readOnly is unnecessary when readOnlyOption '${readOnlyOptionValue}' is declared.` ) } return readOnlyValue ?? !!readOnlyOptionValue }) .def(false), // Pair read_only_option with read_only so the platform's // DictionaryReadOnlyOptionListener doesn't clear read_only on subsequent // installs when it sees a previously-set option go to null. read_only_option: $.from('readOnly', 'readOnlyOption').map( (readOnly, readOnlyOption) => { const opt = readOnlyOption.ifString()?.getValue() if (opt) { return opt } return readOnly.ifBoolean()?.getValue() === true ? 'instance_configured' : undefined } ), reference: $.from('referenceTable'), spell_check: $.from('spellCheck').toBoolean().def(false), table_reference: $.from('tableReference').toBoolean().def(false), text_index: $.from('textIndex').toBoolean().def(false), unique: $.toBoolean().def(false), widget: $, xml_view: $.from('xmlView').toBoolean().def(false), formula: $.from('formula').toCdata(), virtual_type: $.from('virtualType', 'formula') .map((virtualType, formula) => { const explicit = virtualType.ifString()?.getValue() const hasFormula = formula.isString() && formula.getValue() !== '' const derived = hasFormula ? 'formula' : 'script' if (explicit && explicit !== derived) { const reason = hasFormula ? `formula is set, implying 'formula'` : `formula is not set, implying 'script'` diagnostics.warn( virtualType, `virtualType is set to '${explicit}' but ${reason}. The explicit value '${explicit}' will be used.` ) return explicit } return derived }) .def('script'), use_reference_qualifier: $.from('useReferenceQualifier', 'referenceQual') .map((useReferenceQualifier, referenceQual) => { const explicit = useReferenceQualifier.ifString()?.getValue() const hasDynamic = !!dynamicRefQualRef const hasRef = referenceQual.isString() && referenceQual.getValue() !== '' const derived = hasDynamic ? 'dynamic' : hasRef ? 'advanced' : 'simple' if (explicit && explicit !== derived) { const reason = hasDynamic && hasRef ? `both dynamicRefQual and referenceQual are set` : hasDynamic ? `dynamicRefQual is set, implying 'dynamic'` : hasRef ? `referenceQual is set, implying 'advanced'` : `neither dynamicRefQual nor referenceQual is set, implying 'simple'` diagnostics.hint( useReferenceQualifier, `useReferenceQualifier is set to '${explicit}' but ${reason}. The explicit value '${explicit}' will be used.` ) return explicit } return derived }) .def('simple'), dynamic_ref_qual: $.val(dynamicRefQualRef), })), }) ).with(...documentationRecords, ...choiceRecords, ...choiceSetRecords), } }, }, ], }) export function columnToCallExpression( column: Record, { choices, documentation, defaultLanguage }: { choices: Record[]; documentation: Record[]; defaultLanguage: string } ): CallExpressionShape { const internalType = column.get('internal_type').asString().getValue() // Normalize string_full_utf8 to string for column type lookup const normalizedType = internalType === 'string_full_utf8' ? 'string' : internalType const callExpression = COLUMN_TYPE_TO_API[normalizedType] ?? GenericColumn.name return new CallExpressionShape({ source: column, callee: callExpression, args: [ column .transform(({ $ }) => ({ active: $.toBoolean().def(true), array: $.toBoolean().def(false), attributes: $.map((attributes) => { if (!attributes.isString()) { return undefined } const result: { [key: string]: string | boolean } = {} attributes .toString() .getValue() .split(',') .forEach((attr) => { if (attr === '') { return } const [key, value] = attr.split('=').map((s) => s.trim()) // Filter out special parameters that have their own fields if (key === 'scale' || key === 'script') { return } if (!key || value === undefined) { return } if (value === 'true') { result[key] = true } else if (value === 'false') { result[key] = false } else { result[key] = value } }) return result }).def({}), // Extract special parameters from attributes scale: $.from('attributes').map((attributes) => { if (!attributes.isString()) { return undefined } const scaleMatch = attributes .asString() .getValue() .match(/scale=([^,]+)/) return scaleMatch ? Number(scaleMatch[1]) : undefined }), script: $.from('attributes').map((attributes) => { if (!attributes.isString()) { return undefined } const scriptMatch = attributes .asString() .getValue() .match(/script=([^,]+)/) return scriptMatch ? scriptMatch[1] : undefined }), source_table: $.from('attributes').map((attributes) => { if (!attributes.isString()) { return undefined } const sourceTableMatch = attributes .asString() .getValue() .match(/source_table=([^,]+)/) return sourceTableMatch ? sourceTableMatch[1] : undefined }), isFullUTF8: $.from('internal_type').map((internalType) => internalType?.isString() && internalType.asString().getValue() === 'string_full_utf8' ? true : undefined ), audit: $.toBoolean().def(false), cascadeRule: $.from('reference_cascade_rule').def(''), columnType: callExpression === GenericColumn.name ? $.from('internal_type') : $.val(undefined), default: $.from('default_value') .map((defaultValue) => { // TODO Solve default type inference generally if (callExpression === BooleanColumn.name) { if (defaultValue.isBoolean()) { return defaultValue } else if (defaultValue.isString()) { const defaultString = defaultValue.asString().getValue().trim() if (defaultString === 'true') { return true } if (defaultString === 'false') { return false } } } if (defaultValue.isNumber()) { return defaultValue.getValue().toString() } return defaultValue }) .def(''), choices: choices.length ? $.val(generateChoices(choices, defaultLanguage)) : $.val(undefined), dropdown: $.from('choice') .map((choice) => { let dropdownIndex = -1 if (choice.isNumber()) { dropdownIndex = choice.asNumber().getValue() } else if (choice.isString()) { const parsedChoice = Number(choice.asString().getValue()) if (!isNaN(parsedChoice)) { dropdownIndex = parsedChoice } } return dropdownIndex !== -1 ? choiceDropdown[dropdownIndex] : undefined }) .def('none'), dynamicValueDefinitions: $.from( 'calculation', 'choice_table', 'choice_field', 'virtual_type', 'virtual' ).map((calculation, choiceTable, choiceField, virtualType, virtual) => { // Server only reads calculation when virtual_type='script' and virtual=true const vType = virtualType.ifString()?.getValue() const isVirtual = virtual.ifBoolean()?.getValue() ?? virtual.ifString()?.getValue() === 'true' if ( isVirtual && vType !== 'formula' && calculation.ifString()?.getValue() && calculation.asString().getValue() !== DEFAULT_COLUMN_CALCULATION ) { return new ObjectShape({ source: calculation, properties: { type: 'calculated_value', calculatedValue: calculation, }, }).withAliasedKeys(calculationAliases) } if (choiceTable.ifString()?.getValue() && choiceField.ifString()?.getValue()) { return { type: 'choices_from_other_table', table: choiceTable.asString().getValue(), field: choiceField.asString().getValue(), } } return undefined }), // Direct dependent parameter for DocumentIdColumn, TemplateValueColumn, and FieldListColumn dependent: $.from('dependent').map((dependent) => { if (dependent?.isString()) { const value = dependent.asString().getValue() return value !== '' ? value : undefined } return undefined }), elementReference: $.from('element_reference').toBoolean().def(false), functionDefinition: $.from('function_definition').def(''), label: documentationToLabelShape( $, documentation, column.get('column_label').ifString()?.getValue(), generateLabel(column.get('element').asString().getValue()), defaultLanguage ), mandatory: $.toBoolean().def(false), maxLength: $.from('max_length') .map((maxLength) => { if (maxLength.isNumber()) { return maxLength.asNumber() } if (maxLength.isString()) { const numVal = parseInt(maxLength.asString().getValue()) return isNaN(numVal) ? maxLength : numVal } return undefined }) .def(''), primary: $.toBoolean().def(false), readOnly: $.from('read_only').toBoolean().def(false), readOnlyOption: $.from('read_only_option').def(''), referenceTable: $.from('reference').def(''), referenceFloats: $.from('reference_floats').toBoolean().def(false), referenceKey: $.from('reference_key').def(''), mtom: $.from('mtom').def(''), referenceQual: $.from('reference_qual').def(''), dynamicCreation: $.from('dynamic_creation').toBoolean().def(false), dynamicCreationScript: $.from('dynamic_creation_script').def(''), spellCheck: $.from('spell_check').toBoolean().def(false), tableReference: $.from('table_reference').toBoolean().def(false), textIndex: $.from('text_index').toBoolean().def(false), unique: $.toBoolean().def(false), widget: $.def(''), xmlView: $.from('xml_view').toBoolean().def(false), formula: $.from('virtual_type', 'formula').map((virtualType, formula) => { // Server only reads formula when virtual_type='formula' if (virtualType.ifString()?.getValue() !== 'formula') { return undefined } return formula.ifString()?.ifNotEmpty() }), virtualType: $.from('virtual_type', 'formula').map((virtualType, formula) => { const qualifier = virtualType.ifString()?.getValue() const hasFormula = formula.isString() && formula.getValue() !== '' if (hasFormula && qualifier === 'formula') { return undefined } if (!hasFormula && (!qualifier || qualifier === 'script')) { return undefined } return qualifier }), useReferenceQualifier: $.from('use_reference_qualifier', 'dynamic_ref_qual', 'reference_qual').map( (useReferenceQualifier, dynamicRefQual, referenceQual) => { const qualifier = useReferenceQualifier.ifString()?.getValue() const hasDynamicRefQual = dynamicRefQual.isString() && dynamicRefQual.getValue() !== '' const hasReferenceQual = referenceQual.isString() && referenceQual.getValue() !== '' if (hasDynamicRefQual && qualifier === 'dynamic') { return undefined } if (hasReferenceQual && qualifier === 'advanced') { return undefined } if (!hasDynamicRefQual && !hasReferenceQual && (!qualifier || qualifier === 'simple')) { return undefined } // A qualifier field is populated but use_reference_qualifier doesn't select // it (absent, 'simple', or pointing at the other qualifier type) - that // qualifier data is inert on the platform. Emit the real value explicitly // (defaulting a truly absent field to 'simple') so a rebuild can't mistake // inert leftover data (e.g. from a hand-authored bootstrap XML that never // set use_reference_qualifier) for developer intent to activate it. return qualifier ?? 'simple' } ), dynamicRefQual: $.from('dynamic_ref_qual').def(''), })) .withAliasedKeys(columnAliases), ], }) } // Taken from DBNamePrefixes.java function stripPrefix(name: string) { if (name.startsWith('u_')) { return name.substring(2) } if (name.startsWith('sn_')) { return name.substring(3) } if (name.startsWith('x_')) { name = name.substring(2) const firstUnderscore = name.indexOf('_') if (firstUnderscore > 0) { name = name.substring(firstUnderscore + 1) } } return name } // Taken from LabelGenerator.java function applyAcronyms(label: string) { return label .split(' ') .map((word) => (ACRONYMS.has(word.toLowerCase()) ? word.toUpperCase() : word)) .join(' ') } // Taken from LabelGenerator.java export function generatePlural(label: string): string { for (const suffix of PLURAL_EQUALS_LABEL) { if (label.endsWith(suffix)) { return label } } for (const suffix of PLURAL_SPECIAL_CASE_Y_TO_IES) { if (label.endsWith(suffix)) { return `${label.substring(0, label.lastIndexOf('y'))}ies` } } for (const suffix of PLURAL_SPECIAL_CASE_ADD_ES) { if (label.endsWith(suffix)) { return `${label}es` } } return `${label}s` } // Taken from LabelGenerator.java export function generateLabel(name: string): string { return applyAcronyms(_.startCase(stripPrefix(name))) } export function documentationToLabelShape( $: ShapeTransform, documentation: Record[], labelFromParent: string | undefined, defaultLabel: string, defaultLanguage: string ) { const labels = documentation.map((doc) => { const label = doc.get('label').asString().getValue() return doc .transform(({ $ }) => ({ label: $.def(defaultLabel), plural: $.def(generatePlural(label)), language: $.def(defaultLanguage), hint: $.def(''), help: $.def(''), url: $.def(''), urlTarget: $.from('url_target').def(''), })) .withAliasedKeys(documentationAliases) }) const [firstLabel] = labels const nonDefaultFirstLabelKeys = firstLabel?.keys(true) ?? [] const [firstNonDefaultKey, ...otherNonDefaultKeys] = nonDefaultFirstLabelKeys return !firstLabel ? $.val(labelFromParent).def(defaultLabel) // No documentation provided, so just use the label from the parent record : labels.length === 1 && otherNonDefaultKeys.length < 1 && (firstNonDefaultKey === 'label' || firstNonDefaultKey === undefined) ? $.val(firstLabel.get('label')).def(defaultLabel) // One documentation record with only the label value changed, so use shorthand syntax : $.val(labels.filter((label) => label.keys(true).length > 0)).def([]) } export async function labelShapeToDocumentation( labelShape: Shape, tableName: string, columnName: string | undefined, defaultLanguage: string, factory: Factory, diagnostics: Diagnostics ): Promise { const labels: Shape[] = labelShape.isArray() ? labelShape.getElements() : labelShape.isString() ? [Shape.from(labelShape, { label: labelShape })] : [Shape.from(labelShape, {})] const defaultLabel = generateLabel(columnName ?? tableName) const documentation: Record[] = [] const languages = new Set() for (const label of labels) { const labelObject = label.asObject().withAliasedKeys(documentationAliases) generateDeprecatedDiagnostics(labelObject, diagnostics) const language = labelObject.get('language').ifString()?.getValue() || defaultLanguage if (!BCP47_LANGUAGE_TAG_PATTERN.test(language)) { diagnostics.error( label.getOriginalNode(), `'language' must be a valid BCP 47 language tag (e.g., 'en', 'es', 'en-US', 'zh-Hans')` ) } if (languages.has(language)) { diagnostics.error( label.getOriginalNode(), `Duplicate language '${language}' found. Each language must be unique per label entry.` ) } languages.add(language) const labelValue = labelObject.get('label').ifString()?.ifNotEmpty()?.getValue() ?? defaultLabel documentation.push( await factory.createRecord({ source: label, table: 'sys_documentation', properties: labelObject.transform(({ $ }) => ({ name: $.val(tableName), element: $.val(columnName), label: $.val(labelValue).def(defaultLabel), plural: $.def(generatePlural(labelValue)), language: $.def(defaultLanguage), help: $, hint: $, url: $, url_target: $.from('urlTarget'), })), }) ) } return documentation }