import { CallExpressionShape, Plugin, type Shape, type StringShape } from '@servicenow/sdk-build-core' import { NowIdShape } from '../now-id-plugin' import { NowIncludeShape } from '../now-include-plugin' import { ModuleFunctionShape } from '../server-module-plugin' import { toReference, validateServerScriptField } from '../utils' const DEFAULT_POST_PROCESS_SCRIPT = `(function execute(aliasId, connectionSysId, jsonDefaultData, jsonDynamicData) { // aliasId: sys_id of the alias record // connectionSysId: sys_id of the connection record created by using the default and dynamic data // jsonDefaultData (String): The default JSON template data from template table // e.g. var jsonDefaultParsed = JSON.parse(jsonDefaultData); // gs.info("default connection url = " + jsonDefaultParsed["connection"]["connection_url"]); // jsonDynamicData (String): User input based on the key/values defined in "Dynamic Data" // e.g. var jsonDynamicParsed = JSON.parse(jsonDynamicData); // gs.info("dynamic connection url = " + jsonDynamicParsed["connection.connection_url"]); })(aliasId, connectionSysId, jsonDefaultData, jsonDynamicData); ` const DEFAULT_PRE_EDIT_SCRIPT = `(function execute(aliasId, connectionSysId, jsonDefaultData, jsonDynamicData) { // aliasId: sys_id of the alias record. // connectionSysId: sys_id of the connection record created by using the default and dynamic data. // jsonDefaultData (String): The default JSON template data from template table. // e.g. var jsonDefaultParsed = JSON.parse(jsonDefaultData); // gs.info("default connection url = " + jsonDefaultParsed["connection"]["connection_url"]); // jsonDynamicData (String): Current values of Connection and Credential fields (except Password fields) based on the key-value pairs defined in "Dynamic Data". // e.g. var jsonDynamicParsed = JSON.parse(jsonDynamicData); // gs.info("dynamic connection url = " + jsonDynamicParsed["connection.connection_url"]); //returns array of objects. Each object has name-value pairs for populating additional fields values in connections dashboard in the edit view. //In this example script, additionalField1, additionalField2 are additional fields. //Here we define calcAdditionalField1(), calcAdditionalField2() functions in the script //to calculate current values for additionalField1, additionalField2. //return [{ // name: "additionalField1" // value: calcAdditionalField1(), // }, // { // name: "additionalField2" // value: calcAdditionalField2(), // } //]; return []; })(aliasId, connectionSysId, jsonDefaultData, jsonDynamicData); ` const DEFAULT_ON_EDIT_SCRIPT = `(function execute(aliasId, connectionSysId, jsonDefaultData, jsonDynamicData) { // aliasId: sys_id of the alias record // connectionSysId: sys_id of the connection record created by using the default and dynamic data // jsonDefaultData (String): The default JSON template data from template table // e.g. var jsonDefaultParsed = JSON.parse(jsonDefaultData); // gs.info("default connection url = " + jsonDefaultParsed["connection"]["connection_url"]); // jsonDynamicData (String): User input based on the key/values defined in "Dynamic Data" // e.g. var jsonDynamicParsed = JSON.parse(jsonDynamicData); // gs.info("dynamic connection url = " + jsonDynamicParsed["connection.connection_url"]); })(aliasId, connectionSysId, jsonDefaultData, jsonDynamicData); ` function camelToSnake(key: string): string { return key.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`) } function snakeToCamel(key: string): string { return key.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase()) } // Only these keys differ between DB (snake_case) and Fluent API (camelCase). // All other keys — including arbitrary user-defined values in open [key: string]: unknown // sections — are left unchanged to avoid corrupting non-SDK field names. const DB_SNAKE_KEYS = new Set([ 'connection_fields', 'credential_fields', 'additional_fields', 'use_mid', 'connection_url', 'default_group', ]) const FLUENT_CAMEL_KEYS = new Set([ 'connectionFields', 'credentialFields', 'additionalFields', 'useMid', 'connectionUrl', 'defaultGroup', ]) function convertKeys(obj: unknown, converter: (key: string) => string, definedKeys: Set): unknown { if (obj === null || typeof obj !== 'object') { return obj } if (Array.isArray(obj)) { return obj.map((item) => convertKeys(item, converter, definedKeys)) } const result: Record = {} for (const [key, value] of Object.entries(obj as Record)) { const newKey = definedKeys.has(key) ? converter(key) : key result[newKey] = convertKeys(value, converter, definedKeys) } return result } /** * Parse JSON string from DB and convert snake_case keys to camelCase (DB → Fluent) * Used in toShape for dynamicDataSchema and defaultDataTemplate */ function parseDbJson(v: StringShape | undefined): unknown { const parsed = v?.ifNotEmpty()?.parseJson() if (!parsed) { return undefined } const db = parsed.getValue() if (db === undefined || db === null) { return undefined } return convertKeys(db, snakeToCamel, DB_SNAKE_KEYS) } /** * Convert camelCase keys to snake_case and stringify to JSON (Fluent → DB) * Used in toRecord for dynamicDataSchema and defaultDataTemplate */ function serializeFluentJson(v: unknown): string | undefined { if (v === undefined || v === null) { return undefined } return JSON.stringify(convertKeys(v, camelToSnake, FLUENT_CAMEL_KEYS), null, 2) } /** * Helper function to handle ModuleFunctionShape transformation for alias template scripts * Converts module function references to function call strings with proper parameters */ function mapScriptWithModuleSupport(v: Shape) { return ( v .if(ModuleFunctionShape) ?.toString( (n: string) => `${n}({{PARAMS}})`, ['aliasId', 'connectionSysId', 'jsonDefaultData', 'jsonDynamicData'] ) ?? v ) } export const AliasTemplatePlugin = Plugin.create({ name: 'AliasTemplatePlugin', records: { sys_alias_templates: { async toShape(record, { transform }) { // Only call NowIncludeShape.fromRecord for non-empty, non-default script fields. // Scripts that match the default are omitted so round-trips stay clean. const preEditScriptField = record.get('pre_edit_script').ifString()?.ifNotEmpty() const onEditScriptField = record.get('on_edit_script').ifString()?.ifNotEmpty() const postProcessScriptField = record.get('post_process_script').ifString()?.ifNotEmpty() const preEditScript = preEditScriptField && preEditScriptField.getValue().trim() !== DEFAULT_PRE_EDIT_SCRIPT.trim() ? await NowIncludeShape.fromRecord(record, preEditScriptField, transform) : undefined const onEditScript = onEditScriptField && onEditScriptField.getValue().trim() !== DEFAULT_ON_EDIT_SCRIPT.trim() ? await NowIncludeShape.fromRecord(record, onEditScriptField, transform) : undefined const postProcessScript = postProcessScriptField && postProcessScriptField.getValue().trim() !== DEFAULT_POST_PROCESS_SCRIPT.trim() ? await NowIncludeShape.fromRecord(record, postProcessScriptField, transform) : undefined return { success: true, value: new CallExpressionShape({ source: record, callee: 'AliasTemplate', args: [ record.transform(({ $ }) => ({ $id: $.val(NowIdShape.from(record)), name: $, // JSON strings stored in DB → typed objects in Fluent code // Only defined SDK keys are converted from snake_case to camelCase dynamicDataSchema: $.from('dynamic_data_schema').map((v) => parseDbJson(v.ifString())), defaultDataTemplate: $.from('default_data_template').map((v) => parseDbJson(v.ifString()) ), // Script fields via NowIncludeShape for IDE file support preEditScript: $.val(preEditScript), onEditScript: $.val(onEditScript), postProcessScript: $.val(postProcessScript), // Reference field — omit when empty testAction: $.from('test_action').def(''), })), ], }), } }, }, }, shapes: [ { shape: CallExpressionShape, fileTypes: ['fluent'], async toRecord(callExpression, { factory, config, diagnostics }) { if (callExpression.getCallee() !== 'AliasTemplate') { return { success: false } } const arg = callExpression.getArgument(0).asObject() validateServerScriptField(arg.get('preEditScript'), diagnostics, config.serverModulesDir) validateServerScriptField(arg.get('onEditScript'), diagnostics, config.serverModulesDir) validateServerScriptField(arg.get('postProcessScript'), diagnostics, config.serverModulesDir) return { success: true, value: await factory.createRecord({ source: callExpression, table: 'sys_alias_templates', explicitId: arg.get('$id'), properties: arg.transform(({ $ }) => ({ name: $, // Typed objects in Fluent code → JSON strings for DB // Only defined SDK keys are converted from camelCase to snake_case dynamic_data_schema: $.from('dynamicDataSchema').map((v) => serializeFluentJson(v.ifDefined() ? v.getValue() : undefined) ), default_data_template: $.from('defaultDataTemplate').map((v) => serializeFluentJson(v.ifDefined() ? v.getValue() : undefined) ), // Script fields — CDATA wraps both inline strings and Now.include() content // Support for app_modules via ModuleFunctionShape pre_edit_script: $.from('preEditScript') .map(mapScriptWithModuleSupport) .toCdata() .def(DEFAULT_PRE_EDIT_SCRIPT), on_edit_script: $.from('onEditScript') .map(mapScriptWithModuleSupport) .toCdata() .def(DEFAULT_ON_EDIT_SCRIPT), post_process_script: $.from('postProcessScript') .map(mapScriptWithModuleSupport) .toCdata() .def(DEFAULT_POST_PROCESS_SCRIPT), // Reference field — extract sys_id from Record ref or plain string test_action: $.from('testAction').map((v) => toReference(v)), })), }), } }, }, ], })