import { CallExpressionShape, Plugin, type Record, type ObjectShape, type Shape, type Transform, } from '@servicenow/sdk-build-core' import { NowIdShape } from './now-id-plugin' import { ModuleFunctionShape } from './server-module-plugin' import { NowIncludeShape } from './now-include-plugin' import { validateServerScriptField } from './utils' // ============================================================================ // CONSTANTS & CONFIGURATION // ============================================================================ /** enforce_mandatory_fields: TypeScript → XML */ const ENFORCE_MANDATORY_TO_XML: { [k: string]: string } = { onlyMappedFields: 'Only Mapped Fields', allFields: 'All Fields', no: 'No', } /** enforce_mandatory_fields: XML → TypeScript */ const ENFORCE_MANDATORY_TO_TS: { [k: string]: string } = { 'Only Mapped Fields': 'onlyMappedFields', 'All Fields': 'allFields', No: 'no', } /** * Default script template for transform map scripts */ const DEFAULT_TRANSFORM_MAP_SCRIPT = '(function transformRow(source, target, map, log, isUpdate) {\n\n\t// Add your code here\n\n})(source, target, map, log, action==="update");' /** * Default script template for transform entry source scripts */ const DEFAULT_TRANSFORM_ENTRY_SCRIPT = `answer = (function transformEntry(source) { // Add your code here return ""; // return the value to be put into the target field })(source);` /** * Default script template for transform scripts */ const DEFAULT_TRANSFORM_SCRIPT = `(function runTransformScript(source, map, log, target /*undefined onStart*/ ) { // Add your code here })(source, map, log, target);` // ============================================================================ // HELPER FUNCTIONS // ============================================================================ /** * Configuration for a transform entry field mapping * This is the result of calling .properties() on a transformed shape */ type FieldConfiguration = { [key: string]: Shape } /** * Result of buildFieldConfig - partial field configuration with sourceField */ interface FieldConfigResult { sourceField: string coalesce?: boolean coalesceCaseSensitive?: boolean coalesceEmptyFields?: boolean choiceAction?: string sourceScript?: unknown // Can be string or NowIncludeShape for module references useSourceScript?: boolean dateFormat?: string referenceValueField?: string } /** * Checks if a field configuration has any non-default properties * @param config - The field configuration object with Shape values * @returns true if any additional properties are set */ function hasAdditionalProperties(config: FieldConfiguration): boolean { return !!( config['coalesce']?.asBoolean()?.getValue() || config['coalesceCaseSensitive']?.asBoolean()?.getValue() || config['coalesceEmptyFields']?.asBoolean()?.getValue() || (config['choiceAction']?.asString()?.getValue() && config['choiceAction'].asString().getValue() !== '') || config['sourceScript']?.getValue() || config['useSourceScript']?.asBoolean()?.getValue() || config['dateFormat']?.asString()?.getValue() || config['referenceValueField']?.asString()?.getValue() ) } /** * Builds a field configuration object with only non-default properties * @param sourceField - The source field name * @param config - The complete field configuration with Shape values * @returns A minimal field configuration object */ function buildFieldConfig(sourceField: string, config: FieldConfiguration): FieldConfigResult { const fieldConfig = Object.entries(config) .filter(([k, v]) => { // Filter out properties that are undefined or have default values if (!v || k === 'sourceField' || k === 'targetField') { return false } const value = v.getValue?.() return value !== undefined && value !== false && value !== '' }) .reduce( (acc, [k, v]) => { // Extract raw value from Shape acc[k] = v.getValue() return acc }, { sourceField } as { [key: string]: unknown; sourceField: string } ) as FieldConfigResult return fieldConfig } /** * Maps sys_transform_entry records to a fields object for the Fluent API * Converts database records into a simplified object structure where: * - Simple mappings: { targetField: "sourceField" } * - Complex mappings: { targetField: { sourceField: "...", coalesce: true, ... } } * * @param entries - Array of sys_transform_entry records * @param transform - Transform context for creating NowIncludeShape * @returns Object mapping target fields to their configurations */ async function mapTransformEntriesToFields( entries: Record[], transform: Transform ): Promise<{ [key: string]: string | FieldConfigResult }> { const fieldsObject: { [key: string]: string | FieldConfigResult } = {} for (const entryRecord of entries) { // Extract sourceScript using NowIncludeShape for module and Now.include support const sourceScript = await NowIncludeShape.fromRecord(entryRecord, entryRecord.get('source_script'), transform) const entry = entryRecord.transform(({ $ }) => ({ // $id: $.val(NowIdShape.from(entryRecord)), sourceField: $.from('source_field').def(''), targetField: $.from('target_field').def(''), coalesce: $.from('coalesce').toBoolean().def(false), coalesceCaseSensitive: $.from('coalesce_case_sensitive').toBoolean().def(false), coalesceEmptyFields: $.from('coalesce_empty_fields').toBoolean().def(false), choiceAction: $.from('choice_action').def(''), sourceScript: $.val(sourceScript).def(''), useSourceScript: $.from('use_source_script').toBoolean().def(false), dateFormat: $.from('date_format').def(''), referenceValueField: $.from('reference_value_field').def(''), })) const targetField = entry.get('targetField').ifString()?.getValue() ?? '' const sourceField = entry.get('sourceField').ifString()?.getValue() ?? '' // Skip entries without a valid target field if (!targetField || targetField.trim() === '') { continue } // Extract all field properties using .properties() for cleaner code const fieldProperties = entry.properties() // Determine if this is a simple or complex mapping if (hasAdditionalProperties(fieldProperties)) { // Complex mapping: allow empty sourceField if scripts/config drive the value fieldsObject[targetField] = buildFieldConfig(sourceField, fieldProperties) } else { // Simple string mapping: require non-empty sourceField if (sourceField && sourceField.trim() !== '') { fieldsObject[targetField] = sourceField } } } return fieldsObject } /** * Maps sys_transform_script records to an array of script configurations * Returns undefined if no scripts exist (for cleaner API output) * * @param scripts - Array of sys_transform_script records * @param transform - Transform context for creating NowIncludeShape * @returns Array of script configurations or undefined if empty */ async function mapTransformScriptsToShape(scripts: Record[], transform: Transform): Promise { if (scripts.length === 0) { return undefined } const scriptShapes: ObjectShape[] = [] for (const scriptRecord of scripts) { const script = await NowIncludeShape.fromRecord(scriptRecord, scriptRecord.get('script'), transform) scriptShapes.push( scriptRecord.transform(({ $ }) => ({ $id: $.val(NowIdShape.from(scriptRecord)), active: $.toBoolean().def(true), order: $.map((v) => v.ifString()?.ifNotEmpty()?.toNumber()).def(100), when: $.def('onAfter'), script: $.val(script).def(DEFAULT_TRANSFORM_SCRIPT), })) ) } return scriptShapes } // ============================================================================ // PLUGIN DEFINITION // ============================================================================ export const ImportSetPlugin = Plugin.create({ name: 'ImportSetPlugin', records: { sys_transform_entry: { coalesce: ['map', 'target_field'], }, sys_transform_script: {}, sys_transform_map: { relationships: { sys_transform_entry: { via: 'map', descendant: true, }, sys_transform_script: { via: 'map', descendant: true, }, }, async toShape(record, { descendants, transform }) { const entries = descendants.query('sys_transform_entry') const scripts = descendants.query('sys_transform_script') // Extract main transform map script using NowIncludeShape const mapScript = await NowIncludeShape.fromRecord(record, record.get('script'), transform) // Extract transform scripts using NowIncludeShape const transformScripts = await mapTransformScriptsToShape(scripts, transform) // Extract field mappings with module and Now.include support for source scripts const fieldsObject = await mapTransformEntriesToFields(entries, transform) // Check if runScript is enabled to determine script handling const runScriptValue = record.get('run_script').toBoolean().getValue() // Build the configuration object conditionally const config = record.transform(({ $ }) => { const baseConfig = { $id: $.val(NowIdShape.from(record)), name: $, targetTable: $.from('target_table'), sourceTable: $.from('source_table'), order: $.map((v) => v.ifString()?.ifNotEmpty()?.toNumber()).def(100), active: $.toBoolean().def(false), runBusinessRules: $.from('run_business_rules').toBoolean().def(false), enforceMandatoryFields: $.from('enforce_mandatory_fields') .map((v) => ENFORCE_MANDATORY_TO_TS[v.ifString()?.ifNotEmpty()?.getValue() ?? ''] ?? 'no') .def('no'), copyEmptyFields: $.from('copy_empty_fields').toBoolean().def(false), createOnEmptyCoalesce: $.from('create_new_record_on_empty_coalesce_fields') .toBoolean() .def(false), fields: $.val(fieldsObject).def({}), scripts: $.val(transformScripts).def([]), } // Add script-related properties based on runScript value return { ...baseConfig, runScript: $.val(runScriptValue), ...(runScriptValue && { script: $.val(mapScript).def(DEFAULT_TRANSFORM_MAP_SCRIPT), }), } }) return { success: true, value: new CallExpressionShape({ source: record, callee: 'ImportSet', args: [config], }), } }, }, }, shapes: [ { shape: CallExpressionShape, fileTypes: ['fluent'], async toRecord(callExpression, { factory, diagnostics, config }) { if (callExpression.getCallee() !== 'ImportSet') { return { success: false } } const arg = callExpression.getArgument(0).asObject() // ======================================================================== // VALIDATION // ======================================================================== const targetTableArg = arg.get('targetTable') const targetTable = targetTableArg.ifString()?.getValue() || '' const name = arg.get('name') // Validate script reference can be resolved when runScript is true const runScriptValue = arg.get('runScript') const runScript = runScriptValue.isDefined() ? runScriptValue.toBoolean().getValue() : false const script = arg.get('script') if (runScript) { validateServerScriptField(script, diagnostics, config.serverModulesDir) } // Validate: script should not be provided when runScript is false/undefined if (!runScript && script.isDefined()) { diagnostics.error( script, `Property 'script' cannot be used when 'runScript' is false or undefined. Remove the 'script' property or set 'runScript: true'.` ) } const sourceTable = arg.get('sourceTable').asString()?.getValue() // ======================================================================== // CREATE TRANSFORM MAP RECORD // ======================================================================== const transformMapRecord = await factory.createRecord({ source: callExpression, table: 'sys_transform_map', explicitId: arg.get('$id'), properties: arg.transform(({ $ }) => ({ name: $.val(name.ifString()?.getValue()), source_table: $.val(sourceTable), target_table: $.val(targetTable), active: $.from('active').toBoolean().def(false), run_business_rules: $.from('runBusinessRules').toBoolean().def(false), enforce_mandatory_fields: $.from('enforceMandatoryFields') .map((v) => ENFORCE_MANDATORY_TO_XML[v.ifString()?.getValue() ?? ''] ?? 'No') .def('No'), copy_empty_fields: $.from('copyEmptyFields').toBoolean().def(false), create_new_record_on_empty_coalesce_fields: $.from('createOnEmptyCoalesce') .toBoolean() .def(false), script: $.from('script') .map( (v) => v .if(ModuleFunctionShape) ?.toString( (n) => `${n}({{PARAMS}})`, ['source', 'target', 'map', 'log', 'isUpdate'] ) ?? v ) .toCdata() .def(DEFAULT_TRANSFORM_MAP_SCRIPT), run_script: $.from('runScript').toBoolean().def(false), coalesce: $.from('mapCoalesce').toBoolean().def(false), order: $.toNumber().def(100), })), }) // ======================================================================== // CREATE TRANSFORM ENTRY RECORDS (Field Mappings) // ======================================================================== const fieldsArg = arg.get('fields') const entryRecords: Record[] = [] if (fieldsArg.isDefined()) { const fieldsObject = fieldsArg.asObject() for (const [targetField, fieldShape] of fieldsObject.entries()) { // Handle both simple string mappings and complex objects if (fieldShape.isString()) { // Simple mapping: { targetField: "sourceField" } const sourceField = fieldShape.getValue() entryRecords.push( await factory.createRecord({ source: callExpression, table: 'sys_transform_entry', properties: { map: transformMapRecord, source_field: sourceField, target_field: targetField, choice_action: '', source_script: DEFAULT_TRANSFORM_ENTRY_SCRIPT, use_source_script: false, date_format: '', reference_value_field: '', source_table: sourceTable, target_table: targetTable, coalesce: false, coalesce_case_sensitive: false, coalesce_empty_fields: false, }, }) ) } else if (fieldShape.isObject()) { // Complex mapping: { targetField: { sourceField: "...", coalesce: true, ... } } const fieldObj = fieldShape.asObject() // ============================================================ // FIELD-LEVEL CONSTRAINT VALIDATION // ============================================================ // Validate coalesce options const coalesceValue = fieldObj.get('coalesce') const coalesce = coalesceValue.isDefined() ? coalesceValue.toBoolean().getValue() : false const coalesceCaseSensitive = fieldObj.get('coalesceCaseSensitive') const coalesceEmptyFields = fieldObj.get('coalesceEmptyFields') if (!coalesce && coalesceCaseSensitive.isDefined()) { diagnostics.error( coalesceCaseSensitive, `Field '${targetField}': Property 'coalesceCaseSensitive' cannot be used when 'coalesce' is false or undefined. Remove the 'coalesceCaseSensitive' property or set 'coalesce: true'.` ) } if (!coalesce && coalesceEmptyFields.isDefined()) { diagnostics.error( coalesceEmptyFields, `Field '${targetField}': Property 'coalesceEmptyFields' cannot be used when 'coalesce' is false or undefined. Remove the 'coalesceEmptyFields' property or set 'coalesce: true'.` ) } const useSourceScriptValue = fieldObj.get('useSourceScript') const useSourceScript = useSourceScriptValue.isDefined() ? useSourceScriptValue.toBoolean().getValue() : false const sourceScriptValue = fieldObj.get('sourceScript') // Hint: sourceScript has no effect when useSourceScript is false/undefined if (!useSourceScript && sourceScriptValue.isDefined()) { diagnostics.hint( sourceScriptValue, `Field '${targetField}': 'sourceScript' has no effect when 'useSourceScript' is false. The script will be saved but will not run until 'useSourceScript' is set to true.` ) } // Hint: when useSourceScript is true but no sourceScript provided, default template is used if (useSourceScript && !sourceScriptValue.isDefined()) { diagnostics.hint( useSourceScriptValue, `Field '${targetField}': No 'sourceScript' provided — the default transform entry script will be used. Add a 'sourceScript' to define the transformation logic.` ) } entryRecords.push( await factory.createRecord({ source: callExpression, table: 'sys_transform_entry', properties: fieldObj.transform(({ $ }) => ({ map: $.val(transformMapRecord), source_field: $.from('sourceField').def(''), target_field: $.val(targetField), choice_action: $.from('choiceAction').def(''), source_script: $.from('sourceScript') .map( (v) => v .if(ModuleFunctionShape) ?.toString((n) => `${n}({{PARAMS}})`, ['source']) ?? v ) .toCdata() .def(DEFAULT_TRANSFORM_ENTRY_SCRIPT), use_source_script: $.from('useSourceScript').toBoolean().def(false), date_format: $.from('dateFormat').def(''), reference_value_field: $.from('referenceValueField').def(''), source_table: $.val(sourceTable), target_table: $.val(targetTable), coalesce: $.from('coalesce').toBoolean().def(false), coalesce_case_sensitive: $.from('coalesceCaseSensitive').toBoolean().def(false), coalesce_empty_fields: $.from('coalesceEmptyFields').toBoolean().def(false), })), }) ) } } } // ======================================================================== // CREATE TRANSFORM SCRIPT RECORDS // ======================================================================== const scripts = arg.get('scripts')?.ifArray()?.getElements() ?? [] const scriptRecords: Record[] = [] for (const script of scripts) { const obj = script.asObject() scriptRecords.push( await factory.createRecord({ source: callExpression, table: 'sys_transform_script', explicitId: obj.get('$id'), properties: obj.transform(({ $ }) => ({ map: $.val(transformMapRecord), active: $.toBoolean().def(true), order: $.toNumber().def(100), when: $.from('when').def('onAfter'), script: $.from('script') .map( (v) => v .if(ModuleFunctionShape) ?.toString( (n) => `${n}({{PARAMS}})`, ['source', 'map', 'log', 'target'] ) ?? v ) .toCdata() .def(DEFAULT_TRANSFORM_SCRIPT), })), }) ) } return { success: true, value: transformMapRecord.with(...entryRecords, ...scriptRecords), } }, }, ], })