import { CallExpressionShape, type ObjectShape, Plugin, type ArrayShape } from '@servicenow/sdk-build-core' import { ModuleFunctionShape } from './server-module-plugin' import { NowIdShape } from './now-id-plugin' import { generateDeprecatedDiagnostics, validateServerScriptField } from './utils' import { NowIncludeShape } from './now-include-plugin' // Similar to lodash's groupBy, but simply stores the last element of each group function groupByExistence(collection: T[] | undefined, keyProvider: (element: T) => string): { [key: string]: T } { return ( collection?.reduce( (groups, e) => { groups[keyProvider(e)] = e return groups }, {} as { [key: string]: T } ) ?? {} ) } const createBidiMap = (pairs: [string, string][]) => { const map = new Map(pairs) const reverseMap = new Map(pairs.map(([k, v]) => [v, k] as const)) return { get: (key: string): string | undefined => map.get(key), reverseLookUp: (value: string): string | undefined => reverseMap.get(value), } } const whenBidiMap = createBidiMap([ ['async', 'async_always'], ['async_deprecated', 'async'], ['display', 'before_display'], ['before', 'before'], ['after', 'after'], ]) const brAliases = { addMessage: ['add_message'], abortAction: ['abort_action'], setFieldValue: ['set_field_value'], filterCondition: ['filter_condition'], roleConditions: ['role_conditions'], protectionPolicy: ['sys_policy'], } export const BusinessRulePlugin = Plugin.create({ name: 'BusinessRulePlugin', records: { sys_script: { async toShape(record, { transform }) { const script = await NowIncludeShape.fromRecord(record, record.get('script'), transform) return { success: true, value: new CallExpressionShape({ source: record, callee: 'BusinessRule', args: [ record .transform(({ $ }) => ({ $id: $.val(NowIdShape.from(record)), name: $, table: $.from('collection'), order: $.toNumber().def(100), active: $.toBoolean().def(true), when: $.map((v) => { return v.ifString() ? whenBidiMap.reverseLookUp(v.asString().getValue()) : v }), action: $.val(getActionsArray(record)), message: $.def(''), addMessage: $.from('add_message').toBoolean().def(false), condition: $.def(''), filterCondition: $.from('filter_condition').def(''), description: $.def(''), setFieldValue: $.from('template').def(''), abortAction: $.from('abort_action').toBoolean().def(false), roleConditions: $.from('role_conditions').map((v) => v.ifString()?.ifNotEmpty()?.getValue().split(',') ), script: $.val(script).def(DEFAULT_SCRIPT), priority: $.map((p) => p.ifString()?.ifNotEmpty()?.toNumber()).def(100), changeFields: $.from('change_fields').toBoolean().def(false), clientCallable: $.from('client_callable').toBoolean().def(false), executeFunction: $.from('execute_function').toBoolean().def(false), access: $.toString().def('package_private'), rest: $.from( 'is_rest', 'rest_variables', 'rest_method', 'rest_method_text', 'rest_service', 'rest_service_text' ) .def({ webService: false, variables: '', method: '', methodText: '', service: '', serviceText: '', }) .map( ( is_rest, rest_variables, rest_method, rest_method_text, rest_service, rest_service_text ) => ({ webService: is_rest.ifDefined()?.toBoolean(), variables: rest_variables.toString(), method: rest_method.toString(), methodText: rest_method_text.toString(), service: rest_service.toString(), serviceText: rest_service_text.toString(), }) ), protectionPolicy: $.from('sys_policy').def(''), })) .withAliasedKeys(brAliases), ], }), } }, }, }, shapes: [ { shape: CallExpressionShape, fileTypes: ['fluent'], async toRecord(callExpression, { factory, config, diagnostics }) { if (callExpression.getCallee() !== 'BusinessRule') { return { success: false } } const br = callExpression.getArgument(0).asObject().withAliasedKeys(brAliases) const rest = br.get('rest').ifDefined()?.asObject() generateDeprecatedDiagnostics(br, diagnostics) validateServerScriptField(br.get('script'), diagnostics, config.serverModulesDir) return { success: true, value: await factory.createRecord({ source: callExpression, table: 'sys_script', explicitId: br.get('$id'), properties: br.transform(({ $, merge }) => ({ name: $, order: $.def(100), active: $.def(true), message: $, add_message: $.from('addMessage').def(false), condition: $, filter_condition: $.from('filterCondition'), description: $, advanced: $.val( br .get('script') .pipe((script) => script.isDefined() && script.getValue() !== DEFAULT_SCRIPT) ), script: $.map( (v) => v .if(ModuleFunctionShape) ?.toString((n) => `${n}({{PARAMS}})`, ['current', 'previous']) ?? v ) .toCdata() .def(DEFAULT_SCRIPT), collection: $.from('table'), when: $.map((v) => (v.ifString() ? whenBidiMap.get(v.asString().getValue()) : v)).def( 'before' ), [merge]: $.from('action').map((v) => { const { insertAction, updateAction, deleteAction, queryAction } = groupByExistence( v.ifArray()?.getElements(), (action) => `${action.asString().getValue()}Action` ) return { action_insert: !!insertAction, action_update: !!updateAction, action_query: !!queryAction, action_delete: !!deleteAction, } }), template: $.from('setFieldValue'), role_conditions: $.from('roleConditions').map((v) => getRolesString(v.ifArray())), abort_action: $.from('abortAction').def(false), priority: $.def(100), is_rest: $.val(rest?.get('webService')).def(false), rest_variables: $.val(rest?.get('variables')), rest_method: $.val(rest?.get('method')), rest_method_text: $.val(rest?.get('methodText')), rest_service: $.val(rest?.get('service')), rest_service_text: $.val(rest?.get('serviceText')), change_fields: $.from('changeFields').def(false), client_callable: $.from('clientCallable').def(false), execute_function: $.from('executeFunction').def(false), access: $.def('package_private'), sys_policy: $.from('protectionPolicy'), sys_name: $.from('name'), })), }), } }, }, ], }) type Action = 'insert' | 'update' | 'delete' | 'query' function getActionsArray(recordData: ObjectShape): Action[] { const actions: Action[] = [] if (recordData.get('action_update').toBoolean().equals(true)) { actions.push('update') } if (recordData.get('action_delete').toBoolean().equals(true)) { actions.push('delete') } if (recordData.get('action_insert').toBoolean().equals(true)) { actions.push('insert') } if (recordData.get('action_query').toBoolean().equals(true)) { actions.push('query') } return actions } function getRolesString(roles: ArrayShape | undefined): string | undefined { if (!roles) { return undefined } const uniqueRoles = [ ...new Set( roles .getElements() .map((role) => (role.isRecord() ? role.get('name').asString().getValue() : role.asString().getValue())) ), ] return uniqueRoles.join(',') } // This is the default script on the instance. We should provide // it in the case they don't define a script, as they may later want // to add a script on the instance and it's a better experience to // have a starting script available. export const DEFAULT_SCRIPT = '(function executeRule(current, previous /*null when async*/) {\n\n\t// Add your code here\n\n})(current, previous);'