import { CallExpressionShape, Plugin, IdentifierShape, Record, StringShape, type ObjectShape, type Diagnostics, Database, } from '@servicenow/sdk-build-core' import { create } from 'xmlbuilder2' import { generateDeprecatedDiagnostics } from './utils' const DEFAULT_VIEW = 'Default view' const listElementAliases = { averageValue: ['average_value'], maxValue: ['max_value'], minValue: ['min_value'], } function generateListColumnProps(column: ObjectShape, index: number, diagnostics: Diagnostics) { const props = column .withAliasedKeys(listElementAliases) .transform(({ $ }) => ({ element: $.from('element'), position: $.from('position').map((p) => (p.ifUndefined() ? index : p)), max_value: $.from('maxValue'), min_value: $.from('minValue'), average_value: $.from('averageValue'), sum: $, })) .properties() generateDeprecatedDiagnostics(column, diagnostics) return props } function getListUpdateName(list: Record): string { const { view: viewShape, parent: parentShape, relationship: relShape, name: tableShape } = list.properties() const viewRecordId = viewShape?.isRecord() ? viewShape.getId() : viewShape?.isRecordId() ? viewShape : undefined const parent = parentShape?.isString() ? parentShape.asString().getValue() : undefined const relationship = relShape?.ifDefined() ? (relShape.ifString()?.getValue() ?? relShape.asRecord().getId().getValue()) : undefined const table = tableShape?.isString() ? tableShape.asString().getValue() : undefined let view = viewRecordId?.isDefined() ? ((viewRecordId?.getKey('name') as string) ?? 'NULL') : viewShape?.asString().getValue() view = viewRecordId && (viewRecordId.getKey('name') === DEFAULT_VIEW || viewRecordId.getValue() === DEFAULT_VIEW) ? 'NULL' : view const updateNameFormats = new Map([ ['no-parent-no-relationship', [table, view]], ['parent-no-relationship', [table, parent, view]], ['no-parent-relationship', ['NULL', relationship, view]], ['parent-relationship', [parent, relationship, view]], ]) const key = `${parent ? 'parent' : 'no-parent'}-${relationship ? 'relationship' : 'no-relationship'}` const segments = updateNameFormats.get(key) ?? [] const updateName = ['sys_ui_list', ...segments].join('_').toLocaleLowerCase() const domain = list.get('sys_domain').getValue() return domain !== undefined && domain !== 'global' && domain !== '' ? `${updateName}_${domain}` : updateName } function getRootAttributes(list: Record): globalThis.Record { const attrs: globalThis.Record = {} const requireAttrs = ['table', 'parent', 'relationship', 'sys_domain'] const resolvedKeys = list.getId().resolveKeys() || {} for (const attr of requireAttrs) { const mapAttr = attr === 'table' ? 'name' : attr const value = resolvedKeys[mapAttr] ?? (list.get(mapAttr)?.ifDefined()?.getValue() as string) ?? '' attrs[attr] = value === 'NULL' ? '' : value } const view = list.get('view') const viewId = view?.ifRecord()?.getId() ?? view?.ifRecordId() if (viewId) { const viewVal = viewId.getPrimaryKey() attrs['view'] = viewVal === DEFAULT_VIEW || viewVal === 'NULL' ? '' : viewVal return attrs } const vk = list.getId().getKeys()?.['view'] if (vk) { const viewVal = typeof vk === 'string' ? vk : vk.getPrimaryKey() attrs['view'] = viewVal === DEFAULT_VIEW || viewVal === 'NULL' ? '' : viewVal } return attrs } export const ListPlugin = Plugin.create({ name: 'ListPlugin', records: { sys_ui_list: { composite: true, coalesce: ['name', 'view', 'sys_domain', 'element', 'relationship', 'parent'], relationships: { sys_ui_list_element: { via: 'list_id', descendant: true, }, sys_ui_view: { via: 'view', inverse: true, }, }, getUpdateName: (list) => ({ success: true, value: getListUpdateName(list) }), toShape(record, { descendants }) { return { success: true, value: new CallExpressionShape({ source: record, callee: 'List', args: [ record.transform(({ $ }) => ({ table: $.from('name').map((n) => n.ifString()?.getValue() ?? ''), view: $.map((v) => v.equals(DEFAULT_VIEW) ? new IdentifierShape({ source: record, name: 'default_view' }) : (v.ifString() ?? v.toRecordId()) ), parent: $.def(''), relationship: $.def(''), columns: $.val( descendants .query('sys_ui_list_element') .sort( (a, b) => a.get('position').toNumber().getValue() - b.get('position').toNumber().getValue() ) .map((element) => { const averageValue = element.get('average_value').equals('true') const maxValue = element.get('max_value').equals('true') const minValue = element.get('min_value').equals('true') const sum = element.get('sum').equals('true') if (!averageValue && !minValue && !maxValue && !sum) { // Element can be represented as a simple string because it does not have any special configuration return element.get('element') } else { // Element has special configuration, so we need to return an object return element .transform(({ $ }) => ({ element: $.from('element'), averageValue: $.val(averageValue || undefined), maxValue: $.val(maxValue || undefined), minValue: $.val(minValue || undefined), sum: $.val(sum || undefined), })) .withAliasedKeys(listElementAliases) } }) ), domain: $.from('sys_domain').def('global'), })), ], }), } }, toFile(list, { config, descendants }) { if (!list.has('name')) { // Probably generated from a deleted key return { success: false } } const listName = list.get('name').asString().getValue() const xml = create().ele('record_update') const resolvedKeys = list.getId().resolveKeys() || {} const rootAttrs = getRootAttributes(list) const root = xml.ele('sys_ui_list', rootAttrs) descendants.query('sys_ui_list_element').forEach((element) => { const child = root.ele('sys_ui_list_element', { action: 'INSERT_OR_UPDATE', apply_defaults: 'true', }) child.ele('sys_id').txt(element.getId().getValue()) child.ele('list_id', { display_value: listName, ...resolvedKeys }).txt(list.getId().getValue()) for (const prop of ['element', 'position', 'min_value', 'max_value', 'average_value', 'sum']) { element .get(prop) .ifDefined() ?.toString() .pipe((p) => child.ele(prop).txt(p.getValue())) } }) const child = root.ele('sys_ui_list', { action: list.getAction(), apply_defaults: 'true' }) child.ele('sys_id').txt(list.getId().getValue()) child.ele('sys_scope', { display_value: config.scope }).txt(config.scopeId) const view = list.get('view') if (view.isRecordId() || view.isRecord()) { const viewId = view.isRecordId() ? view : view.getId() child.ele('view', viewId.resolveKeys()).txt(viewId.getValue()) } else { child.ele('view').txt(view.asString().getValue()) } const name = list.get('name') if (name.isDefined()) { child.ele('name').txt(name.asString().getValue()) } const sysPolicy = list.get('sys_policy') if (sysPolicy.ifString()?.ifNotEmpty()) { child.ele('sys_policy').txt(sysPolicy.asString().getValue()) } const parent = list.get('parent') if (parent.isDefined()) { child.ele('parent').txt(parent.asString().getValue()) } const relationship = list.get('relationship') if (relationship.isDefined()) { child .ele('relationship', relationship.ifRecord()?.getId().resolveKeys()) .txt(relationship.ifString()?.getValue() ?? relationship.asRecord().getId().getValue()) } const domain = list.get('sys_domain') if (domain.isString()) { child.ele('sys_domain').txt(domain.asString().getValue()) } return { success: true, value: { source: list, name: `${getListUpdateName(list)}.xml`, category: list.getInstallCategory(), content: xml.end({ prettyPrint: true }), }, } }, async diff(existing, incoming, _, { factory }) { if (incoming.query().length === 0 || existing.query().length === 0) { return { success: true, value: incoming.query().length === 0 ? new Database() : new Database(incoming.query()), } } const changeDatabase = new Database() let hasChanges = false const existingList = existing.query('sys_ui_list')[0] const incomingList = incoming.query('sys_ui_list')[0] const existingElements = existing.query('sys_ui_list_element') const incomingElements = incoming.query('sys_ui_list_element') if (incomingList && existingList) { if (!existingList.strictEquals(incomingList)) { changeDatabase.insert(existingList.merge(incomingList)) hasChanges = true } } else if (incomingList) { changeDatabase.insert(incomingList) hasChanges = true } const markForRemoval: Record[] = [] for (const element of existingElements) { const match = incoming.resolve(element.getId()) if (!match) { hasChanges = true markForRemoval.push(element) } else { hasChanges = hasChanges || !element.strictEquals(match) changeDatabase.insert(element.merge(match)) } } incomingElements.forEach((element) => { const match = changeDatabase.resolve(element.getId()) if (!match) { changeDatabase.insert(element) } }) for (const element of markForRemoval) { const deleteRecord = await factory.createRecord({ source: element.getSource(), table: 'sys_ui_list_element', explicitId: element.getId(), properties: element.properties(), action: 'DELETE', }) changeDatabase.insert(deleteRecord) } return { success: true, value: hasChanges ? changeDatabase : new Database(), } }, }, sys_ui_list_element: { coalesce: ['list_id', 'element'], }, }, shapes: [ { shape: CallExpressionShape, fileTypes: ['fluent'], async toRecord(callExpression, { factory, diagnostics }) { if (callExpression.getCallee() !== 'List') { return { success: false } } const arg = callExpression.getArgument(0).asObject() const columns = arg.get('columns').ifArray()?.getElements() ?? [] const view = arg.get('view') const viewReference = view.isString() ? await factory.createReference({ source: view, table: 'sys_ui_view', keys: { name: view }, }) : view const list = await factory.createRecord({ source: callExpression, table: 'sys_ui_list', explicitId: arg.get('$id'), properties: arg.transform(({ $ }) => ({ name: $.from('table'), view: $.val(viewReference), parent: $, relationship: $.map((v) => v.if([Record, StringShape])), sys_domain: $.from('domain').def('global'), })), }) const columnRecords: Record[] = [] for (const [index, column] of columns.entries()) { columnRecords.push( await factory.createRecord({ source: callExpression, table: 'sys_ui_list_element', properties: { list_id: list, position: index, ...(column.isString() ? { element: column } : generateListColumnProps(column.asObject(), index, diagnostics)), }, }) ) } return { success: true, value: list.with(...columnRecords), } }, }, ], })